Aver
Every project of sufficient complexity eventually builds a domain language for its tests. Aver gives that language a home in the type system.
Before and after
Section titled “Before and after”Without Aver — three test files, three vocabularies, same behavior:
// Unit testtest('create task', () => { board.create('Fix bug') expect(board.get('Fix bug').status).toBe('backlog')})// API testtest('create task', async () => { await request(app).post('/tasks').send({ title: 'Fix bug' }) const res = await request(app).get('/tasks/Fix bug') expect(res.body.status).toBe('backlog')})// Browser testtest('create task', async ({ page }) => { await page.fill('[data-test=new-task]', 'Fix bug') await page.click('[data-test=create]') await expect(page.locator('[data-test=task-Fix-bug]')).toBeVisible()})# Unit testdef test_create_task(): board.create("Fix bug") assert board.get("Fix bug").status == "backlog"# API testdef test_create_task(client): client.post("/tasks", json={"title": "Fix bug"}) res = client.get("/tasks/Fix bug") assert res.json()["status"] == "backlog"# Browser testdef test_create_task(page): page.fill("[data-test=new-task]", "Fix bug") page.click("[data-test=create]") expect(page.locator("[data-test=task-Fix-bug]")).to_be_visible()# Unit testit "creates a task" do board.create("Fix bug") expect(board.get("Fix bug")[:status]).to eq("backlog")end# API testit "creates a task" do post "/tasks", title: "Fix bug" get "/tasks/Fix bug" expect(json_response[:status]).to eq("backlog")end# Browser testit "creates a task", js: true do fill_in "data-test-new-task", with: "Fix bug" click_button "Create" expect(page).to have_css("[data-test=task-Fix-bug]")end// Unit testfunc TestCreateTask(t *testing.T) { board.Create("Fix bug") assert.Equal(t, "backlog", board.Get("Fix bug").Status)}// API testfunc TestCreateTask(t *testing.T) { resp, _ := http.Post(srv+"/tasks", "application/json", body) task := getTask(srv, "Fix bug") assert.Equal(t, "backlog", task.Status)}// Browser test — chromedp or rodfunc TestCreateTask(t *testing.T) { chromedp.Run(ctx, chromedp.SendKeys("#new-task", "Fix bug"), chromedp.Click("#create"), chromedp.WaitVisible("[data-test=task-Fix-bug]"))}// Unit test#[test]fn create_task() { board.create("Fix bug"); assert_eq!(board.get("Fix bug").status, "backlog");}// API test#[test]fn create_task() { client.post("/tasks").json(&json!({"title": "Fix bug"})).send(); let task: Task = client.get("/tasks/Fix bug").send().json(); assert_eq!(task.status, "backlog");}// Browser test — thirtyfour or fantoccini#[test]fn create_task() { driver.find(By::Css("[data-test=new-task]")).send_keys("Fix bug"); driver.find(By::Css("[data-test=create]")).click(); driver.find(By::Css("[data-test=task-Fix-bug]")).wait_until().displayed();}// Unit test@Test fun `create task`() { board.create("Fix bug") assertEquals("backlog", board.get("Fix bug").status)}// API test@Test fun `create task`() { client.post("/tasks") { setBody("""{"title":"Fix bug"}""") } val task = client.get("/tasks/Fix bug").body<Task>() assertEquals("backlog", task.status)}// Browser test — Selenium/Playwright@Test fun `create task`() { page.fill("[data-test=new-task]", "Fix bug") page.click("[data-test=create]") assertThat(page.locator("[data-test=task-Fix-bug]")).isVisible()}With Aver — one test, every level:
const { test } = suite(taskBoard)
test('create a task in backlog', async ({ when, then }) => { await when.createTask({ title: 'Fix bug' }) await then.taskInStatus({ title: 'Fix bug', status: 'backlog' })})s = suite(TaskBoard)
@s.testdef test_create_task_in_backlog(ctx): ctx.when.create_task(title="Fix bug") ctx.then.task_in_status(title="Fix bug", status="backlog")RSpec.describe TaskBoard do it "creates a task in backlog" do ctx.when.create_task(title: "Fix bug") ctx.then.task_in_status(title: "Fix bug", status: "backlog") endendfunc TestCreateTask(t *testing.T) { suite.Run(t, "create task", func(ctx *aver.TestContext[TaskBoard]) { aver.When(ctx, TaskBoard.CreateTask, CreateTaskPayload{Title: "Fix bug"}) aver.Then(ctx, TaskBoard.TaskInStatus, StatusPayload{Title: "Fix bug", Status: "backlog"}) })}suite.run("create task", |m, tc| { tc.when(&m.create_task, CreateTaskPayload { title: "Fix bug".into() }); tc.then(&m.task_in_status, StatusPayload { title: "Fix bug".into(), status: "backlog".into() });});@Testfun `create task in backlog`() = s.run { ctx -> ctx.act(domain.createTask, "Fix bug") ctx.then(domain.taskInStatus, TaskStatus("Fix bug", "backlog"))} ✓ create a task in backlog [unit] 3ms ✓ create a task in backlog [http] 48ms ✓ create a task in backlog [playwright] 890mstests/test_task_board.py::test_create_task_in_backlog[unit] PASSEDtests/test_task_board.py::test_create_task_in_backlog[http] PASSEDtests/test_task_board.py::test_create_task_in_backlog[playwright] PASSEDTask Board [unit] creates a task in backlogTask Board [http] creates a task in backlogTask Board [playwright] creates a task in backlog=== RUN TestCreateTask/create_task_[unit]=== RUN TestCreateTask/create_task_[http]=== RUN TestCreateTask/create_task_[playwright]test create_task ... oktest create_task_http ... oktest create_task_playwright ... ok✓ create task in backlog [unit]✓ create task in backlog [http]✓ create task in backlog [playwright]Working on business logic? aver run --adapter unit. Touching the API layer? --adapter http. Need full confidence before deploy? --adapter playwright. Same test, right feedback loop for the layer you’re in. How fast the unit adapter runs depends on your design — nullables and dependency injection eliminate IO entirely; a real database keeps fidelity at the cost of speed. Either way, it’s a fraction of the browser.
Why Aver
Section titled “Why Aver”- Right feedback loop, every layer — Same test runs at unit speed during development, HTTP for API contracts, Playwright for full confidence. Pick the level that matches the work.
- Lock in what exists —
approve()captures current behavior as a baseline. Refactor underneath with confidence. - Prove your system is observable — Declare expected telemetry alongside domain operations. Verify spans, attributes, and causal connections in the same test that checks behavior.
- Zero runtime dependencies — Core has no deps. Add protocols as you need them.
Quick start
Section titled “Quick start”npm install --save-dev @averspec/core vitestnpx aver initnpx aver runpip install averspecaver initaver run# RubyGems publish coming soon — install from source:git clone https://github.com/averspec/aver-rb.gitcd aver-rb && bundle installbundle exec aver rungo get github.com/averspec/aver-go# Define domain, adapter, and test in Go filesgo test ./...# crates.io publish coming soon — install from git:# Cargo.toml: averspec = { git = "https://github.com/averspec/aver-rs" }cargo test// Maven Central publish coming soon — use JitPack or build from source:testImplementation("dev.averspec:averspec:0.1.0")./gradlew testOr follow a tutorial: legacy code, greenfield, or telemetry verification.
When Aver is NOT the right tool
Section titled “When Aver is NOT the right tool”- You only need unit tests for a pure function — plain Vitest is simpler
- It’s a prototype or throwaway — the domain layer pays off over time, not day one
- Trivial CRUD with no business rules — if the vocabulary mirrors the schema, there’s nothing to abstract
Read more about when to use Aver →
Implementations
Section titled “Implementations”| Language | Package | Install |
|---|---|---|
| TypeScript | @averspec/core | npm install --save-dev @averspec/core |
| Python | averspec | pip install averspec |
| Go | aver-go | go get github.com/averspec/aver-go |
| Ruby | averspec | Source only — RubyGems coming soon |
| Rust | averspec | Source only — crates.io coming soon |
| Kotlin | averspec | Source only — Maven Central coming soon |
All six implementations share the same behavioral contract model: domains, typed markers, multi-adapter execution, and cross-language telemetry contract verification. Read about how they were built →
Aver tests itself using the same domain-driven architecture it provides. See the test suite.