Skip to main content
Merit is intentionally “pytest-shaped”: you write plain Python, Merit discovers merit_* cases, injects dependencies by parameter name (like pytest fixtures), runs them, and reports results. This page focuses on how to write merits and (importantly) where the behavior lives in the codebase, so you can trust what’s happening.

TLDR - If you know pytest, you already know 80%

Merits

Merit follows pytest-style discovery patterns to find merit functions in your codebase: Files: Merit discovers Python files starting with merit_:
  • merit_chatbot.py
  • merit_agent.py
  • helpers.py
Functions: Inside discovered files, Merit collects functions starting with merit_:
Classes: Classes starting with Merit are discovered, and their merit_* methods become merit cases:

Modifiers

Modifiers are decorators that change how a merit is collected and/or executed. Some modifiers expand a single merit into many cases (like parametrization or dataset iteration), while others change execution semantics (like repeating a case, or marking it as skipped/xfail). Apply them to merit_* functions or Merit* classes. @merit.parametrize(names, values) - Run the same merit with different inputs
@merit.iter_cases(*cases, min_passes=len(cases)) - Iterate over Case objects from external sources, optionally allowing pass thresholds
@merit.iter_case_groups(*groups) - Iterate over CaseGroup objects with per-group thresholds and group-level references
@merit.tag(*tags) - Organize and filter merits by tags
@merit.tag.skip(reason=...) - Skip merits unconditionally
@merit.tag.xfail(reason=..., strict=False) - Mark merits expected to fail
@merit.repeat(n, min_passes=n) - Run merits multiple times to see if AI behavior is consistent
@merit.run_inline - Opt out of default threaded execution for sync merits By default, synchronous merits (def merit_*) run in a worker thread via asyncio.to_thread(...) so the event loop stays responsive. Use @merit.run_inline when a sync merit must run on the main event-loop thread (for example, thread-sensitive libraries).

Resources

Resources are the Merit equivalent of pytest fixtures: named, injectable dependencies that Merit resolves by parameter name. @merit.resource(scope="case") - Define injectable dependencies with lifecycle management
Scopes: "case" (default), "suite", "session". @merit.metric(scope="session") - Define a metric as a scoped, injectable measurement object Metrics behave like resources, but they’re intended to accumulate measurements across many cases and then assert on aggregates at the end of their scope. The most common pattern is to inject a Metric into your merits and use the metrics(...) context manager to record assertion outcomes into that metric.
Scopes: "session" (default), "suite", "case". @merit.sut - Register a System Under Test (SUT) as an injectable callable A SUT is the thing you’re actually evaluating (an agent function, a pipeline, a classifier, a client wrapper, etc.). Declaring it with @merit.sut makes it injectable and traceable, so you can assert not only on the output, but also on how it behaved internally (for example: tool calls).
A SUT must be callable. Merit will inject it into your merit function and you will call it like a normal function (or callable object).

Custom Assert

Merit transforms Python’s assert keyword to provide richer testing capabilities for AI systems. When you run merit files through Merit’s runner, assertions behave differently than standard Python.

Continue on failure (default behavior)

By default, Merit continues running remaining assertions even after one fails. This is different from standard Python, where the first failed assertion stops execution immediately.
All three assertions will be evaluated and reported, even if the first one fails. This behavior lets you see all test failures in a single run rather than fixing them one at a time. To stop on the first failure, use the --fail-fast CLI flag:

Integration with metrics

When assertions are evaluated inside a metrics() context manager, Merit automatically records whether each assertion passed or failed to the specified metrics:

Only works through Merit’s runner

Important: Merit’s assertion transformation only applies when you run files through Merit’s test runner:

Assert messages

Assert messages work as expected and are captured in the AssertionResult:

Merit Functions (Concept)

Discovery rules, organization, and patterns

Quick Start

End-to-end example with SUTs, predicates, metrics, and traces