Caravela's 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.
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
endArgument matchers: :_ wildcards anything, atoms / strings / other
terms compare with ==.
Match qualified remote calls with module::
assert_calls src, :__caravela_policy_field_visible__, [:books, :price, :_],
module: PolicyLibraryThe 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:
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
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.