# Testing Caravela
Two stories on one page:
1. **Testing apps Caravela generates for you** — the ExUnit + Vitest
skeletons `mix caravela.gen.live` emits, and how to fill them in.
Jump to [Generated test skeletons](#generated-test-skeletons).
2. **Testing Caravela itself** — the three-tier approach used inside
this repo to verify the code generators emit the right shape.
Jump to [Tier 1: structural assertions](#tier-1-structural-assertions).
Most users only need §1. §2 is for contributors.
## Generated test skeletons
From v0.13, `mix caravela.gen.live MyApp.Domains.Library` emits a
CI-ready test layer alongside the implementation:
```
test/my_app_web/live/library/book_live_test.exs # :live entities
test/my_app_web/controllers/book_controller_test.exs # :rest entities
assets/svelte/library/BookIndex.test.ts # Vitest per component
assets/svelte/library/BookShow.test.ts
assets/svelte/library/BookForm.test.ts
```
### ExUnit skeletons
Each test file covers one entity. LiveView tests group `describe`
blocks per module (Index / Show / Form); controller tests group one
per HTTP action. Every test carries a single assertion + a `# TODO:`
marker where the app-specific fixture plugs in:
```elixir
defp build_book_fixture(attrs \\ %{}) do
{:ok, entity} = attrs |> Enum.into(%{}) |> Library.create_book(build_context())
entity
end
defp build_context, do: %{current_user: nil}
describe "index" do
test "lists books", %{conn: conn} do
_entity = build_book_fixture()
{:ok, _view, html} = live(conn, "/library/books")
# TODO: assert the fixture is visible in the rendered list.
assert is_binary(html)
end
# …
end
```
The skeletons use the standard Phoenix test stack
(`Phoenix.ConnTest`, `Phoenix.LiveViewTest`) and assume the app has
`*.ConnCase` under `test/support/conn_case.ex` — which `mix phx.new`
emits by default. Pre-wired:
- Structured-error assertions reference
[`Caravela.ChangesetTranslator`](validation.md) so the frontend
contract stays aligned.
- `# TODO:` comments call out exactly where to replace the fixture
factory with your app's ExMachina / factory module / Repo insert.
- Generated code sits above the `# --- CUSTOM ---` marker; anything
you add below survives regeneration ([regeneration](regeneration.md)).
### Vitest skeletons
Colocated next to each Svelte file, using
`@testing-library/svelte`:
```ts
import { render } from '@testing-library/svelte';
import BookIndex from './BookIndex.svelte';
describe('BookIndex', () => {
test('mounts with an empty collection', () => {
const { container } = render(BookIndex, {
props: {
books: [],
loading: false,
flash_message: null,
field_access: { title: true, isbn: true, /* … */ },
actions: { create: true, update: true, delete: true }
}
});
expect(container).toBeTruthy();
});
});
```
These are intentionally thin — a CI oracle that catches "my prop
contract changed and the component now throws", not a full UX
regression suite. Install the dev deps in your app's
`assets/package.json`:
```json
{
"devDependencies": {
"@testing-library/svelte": "^5",
"vitest": "^1"
}
}
```
Run with `npm test` (after wiring `vitest` into your package
scripts).
### Opting out
```bash
mix caravela.gen.live --no-tests MyApp.Domains.Library
```
Skips emission of every test file. The implementation files still
generate normally.
---
# Testing Caravela itself
Caravela's own test suite sits in a corner that most libraries avoid:
we test a code generator. Every assertion has to decide whether it's
checking *the shape* of the generated source or *the behavior* the
source produces. String matching on generated code makes the suite
fragile — a formatter tweak cascades into dozens of test edits — so we
use the three-tier approach below.
## Tier 1: structural assertions
Use these when you want to check "did the template emit a call /
definition / attribute?" without caring about surrounding cosmetics.
### Generated Elixir → `Caravela.ASTAssertions`
Parses the source and walks the AST looking for the specific node
you care about.
```elixir
import Caravela.ASTAssertions
test "list_books pipes through apply_scope + project_fields" do
{_path, src} = Caravela.Gen.Context.render(domain)
# Matches regardless of line wrap, argument formatting, or
# surrounding pipeline shape.
assert_calls src, :apply_scope, [:books, :_]
assert_calls src, :project_fields, [:books, :_]
assert_def src, :list_books, 1
end
```
Argument matchers: `:_` wildcards anything, atoms / strings / other
terms compare with `==`.
Match qualified remote calls with `module:`:
```elixir
assert_calls src, :__caravela_policy_field_visible__, [:books, :price, :_],
module: PolicyLibrary
```
The `module` comparison checks the *last segment* of the alias by
default, so `MyApp.Domains.PolicyLibrary.foo(…)` matches
`module: PolicyLibrary`.
### Generated Svelte / TypeScript → `Caravela.SvelteAssertions`
TS/Svelte AST parsing from Elixir is overkill for what we need.
Instead, normalize every whitespace run to a single space on both
sides before substring matching:
```elixir
import Caravela.SvelteAssertions
assert_contains src, "let { books = [], live } = $props();"
refute_contains src, "hashed_password"
assert_all_contain src, [
"import type { Book, BookFieldAccess, LiveHandle }",
"field_access?: BookFieldAccess;"
]
```
Line breaks and indentation differences are invisible to these
assertions, but the fragment still has to appear *in order*.
## Tier 2: compile + exercise
Sometimes the test isn't "did the template emit X?" — it's "does the
code actually work?". For those, compile the generated source into an
isolated namespace and call into it. [context_integration_test.exs](../test/caravela/context_integration_test.exs)
is the worked example: it renders the context, rewrites the module
aliases into a unique suffix, compiles via `Code.compile_string/1`,
then exercises CRUD round-trips against an in-memory stub Repo.
Use this tier when:
- You're verifying *behavior* across multiple generated files (context
calling into schema calling into Repo stub).
- The rule under test has branches that AST matching alone can't
distinguish ("does this policy deny when the actor lacks the role,
*and* return the right error shape?").
## Tier 3: end-to-end
Not routinely used. A future expansion would spin up an ephemeral
Phoenix app against a test database, run every generator, migrate,
and issue HTTP requests. The cost/benefit usually lands better at
Tier 2 — but this doc will update if the e2e harness lands.
## When to reach for `src =~ "literal"`
Only for tokens that are genuinely positional and stable — CUSTOM
markers, fixed doc strings, the `@generated` header. For anything
that could move under a formatter tweak, prefer Tier 1.