Skip to main content
Prerequisites: Quick Start complete, pip install mellea, Ollama running locally, pytest installed.
Contributing to Mellea itself? See test/README.md for Mellea’s own test markers, fixtures, and CI setup.
Testing generative code requires you to separate concerns: some assertions are always deterministic (the output is the right type), while others depend on model behaviour and are inherently qualitative. This page shows you how to structure both categories, configure the right pytest markers, and make your CI pipeline fast and reliable.

Three levels of assertion

Every test for a @generative function falls into one of four levels: For levels 1–3, use pytest with the patterns below. For semantic evaluation against reference examples — where you want a judge model to score your model’s outputs in bulk — see The unit_test_eval component for Generative Unit Tests at the end of this page. Type and structural checks run in CI. Qualitative checks carry @pytest.mark.qualitative and are skipped in CI when CICD=1 is set.

Setting up a test session fixture

Use a backend fixture to handle CI versus local configuration, and a function-scoped session fixture to give each test a clean slate:
Note: Scoping backend to module and session to function strikes a balance between setup cost and test isolation. Each test gets a clean context, but the backend connection is created once per module.

Module-level markers

Declare markers at the top of your test file with pytestmark so they apply to every test in the module without repetition. Register your own markers in pyproject.toml under [tool.pytest.ini_options] markers to avoid warnings:

Testing @generative functions

Type assertions — always deterministic

The return type of a @generative function is enforced by constrained decoding or output parsing. An isinstance check never depends on model behaviour:

Structural assertions — always deterministic

For Literal return types, membership in the allowed values is enforced before your test sees the result. The assertion is still deterministic:
For Pydantic model return types, assert that the required fields are present and have the right types:

Qualitative assertions — mark and skip in CI

When you want to assert on the content of a response, add @pytest.mark.qualitative. These tests are skipped automatically in CI (CICD=1) and are intended to run locally or in a dedicated quality gate:
Warning: Do not assert on qualitative behaviour without @pytest.mark.qualitative. A deterministic-looking assertion like assert score > 5 can flake across model versions, temperatures, and quantisation levels.

Testing instruct() calls

instruct() calls are non-qualitative when you test structure, not content. Assert that the call returns a value and that the value has the right type:

Inspecting logged model options

_generate_log.model_options lets you confirm that options you passed were forwarded to the model. This is useful when testing custom model option handling:
Note: _generate_log is an internal attribute. Its structure may change between Mellea versions. Use it for debugging and option-forwarding tests, not as a primary correctness check.

Using simple_validate for deterministic checks

simple_validate wraps a plain function into a validation callable that Requirement accepts. Use it to assert deterministic structural constraints inside the IVR loop, or directly in tests to verify that your validator logic behaves correctly:
When you attach simple_validate to a Requirement, it checks the last model output as a string, regardless of how the output was parsed:

The unit_test_eval component for Generative Unit Tests

mellea.stdlib.components.unit_test_eval provides TestBasedEval, a Component that formats an LLM-as-a-judge evaluation task for generative unit testing. You load test cases from a JSON file and pass them to a judge session. This is useful for offline evaluation pipelines, not for individual pytest assertions. Given a task, you provide test cases that consist of evaluation instructions and a set of examples, along with associated metadata. Each example, in conversational format, consists of an input and (optional) target / reference response(s), which can be used to guide evaluation along with the evaluation instructions. They can either be instantiated inline or in JSON format, with a separate JSON file per task. There are no limitations on the number of test examples per task, and each input can have multiple reference responses. The evaluation instructions apply to all the test cases in your task.

JSON file format

Each entry in the JSON array defines one test:

Loading and running evaluations

Note: TestBasedEval calls the judge model once per input. For large evaluation sets, consider batching or running evaluations asynchronously. CLI alternative: The same evaluation can be run without writing Python: m eval run tests/eval_data/email_writer.json --backend ollama --model granite4.1:3b See m eval run --help for full options.

CI strategy

A simple conftest.py that skips qualitative tests in CI:
Then in your GitHub Actions workflow:
To run the full suite including qualitative tests locally:

Next steps