Two stories on one page:
- Testing apps Caravela generates for you — the ExUnit + Vitest
skeletons
mix caravela.gen.liveemits, and how to fill them in. Jump to Generated test skeletons. - 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.
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.tsExUnit 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:
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
# …
endThe 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.ChangesetTranslatorso 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).
Vitest skeletons
Colocated next to each Svelte file, using
@testing-library/svelte:
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:
{
"devDependencies": {
"@testing-library/svelte": "^5",
"vitest": "^1"
}
}Run with npm test (after wiring vitest into your package
scripts).
Opting out
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.
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.