Contents
Same workflow you trust for code — now with a quality gate for prompts
You already unit test your code. A function returns the wrong number, a test fails, and CI blocks the merge. Now picture the same workflow for a chatbot: someone edits a prompt, an answer gets worse, and a failing test stops the deploy.
That workflow exists, and DeepEval makes it feel familiar. It plugs into pytest, scores your LLM outputs with ready-made metrics, and fails the build when quality drops. This tutorial walks you from install to a CI-ready test suite — the testing counterpart to our LLM evaluation overview. DeepEval changes quickly, so the core API here comes from its getting-started documentation, but check the current docs for exact parameter names and new metrics before you copy anything into production.
What DeepEval Does
DeepEval is an open-source Python framework for testing LLM apps. It borrows pytest's structure, so a test looks like a normal test: you build a test case, pick metrics, and assert.
Three ideas drive it:
- Test cases hold the input, the app's actual output, and optional extras like expected output or retrieved context.
- Metrics score a test case, usually from 0 to 1. Many metrics use an LLM as the judge.
- Thresholds turn a score into pass or fail. A test passes when the score meets the threshold.
Because many metrics call a judge model, they cost tokens and carry some variance. Keep that in mind as you set thresholds later.
Install and Set Up
Install the package, and set a key for your judge model. By default, DeepEval's LLM-based metrics call an OpenAI model, so set your OpenAI key. You can also configure another judge model through DeepEval's custom model support:
pip install -U deepeval
export OPENAI_API_KEY="your-key-here"
Create a test file. The test_ prefix matters, because pytest and the DeepEval runner both look for it:
touch test_chatbot.py
Write Your First Test
This test checks whether a chatbot answer is relevant to the question. Replace the hardcoded output with a call to your own app in a moment:
from deepeval import assert_test
from deepeval.test_case import LLMTestCase
from deepeval.metrics import AnswerRelevancyMetric
def test_return_policy_answer():
metric = AnswerRelevancyMetric(threshold=0.5)
test_case = LLMTestCase(
input="What is your return policy?",
actual_output="You can return any item within 30 days for a full refund.",
)
assert_test(test_case, [metric])
Run it with the DeepEval command:
deepeval test run test_chatbot.py
You'll see a results table with the test case, the metric, the score, and a pass or fail status. The runner wraps pytest, so familiar behavior carries over.
Read the Result
Every metric returns a score and, for most LLM-based metrics, a reason that explains the score in plain language. Read the reasons. They show you what the judge noticed, and they often point straight at the fix.
Add Custom Criteria With G-Eval
Built-in metrics cover common needs. For your own quality rules, use G-Eval, which lets you describe the criteria in plain English. The judge then scores the output against your description:
from deepeval import assert_test
from deepeval.test_case import LLMTestCase, LLMTestCaseParams
from deepeval.metrics import GEval
def test_answer_correctness():
correctness = GEval(
name="Correctness",
criteria="The actual output must agree with the expected output on all facts and must not add unsupported claims.",
evaluation_params=[
LLMTestCaseParams.INPUT,
LLMTestCaseParams.ACTUAL_OUTPUT,
LLMTestCaseParams.EXPECTED_OUTPUT,
],
threshold=0.7,
)
test_case = LLMTestCase(
input="How long do I have to return an item?",
actual_output="You have 30 days to return an item.",
expected_output="Customers can return items within 30 days.",
)
assert_test(test_case, [correctness])
G-Eval is the most useful metric in the box. You can encode your own rules: tone, policy compliance, required disclaimers, or anything else you can describe clearly.
Test RAG Apps
Retrieval apps fail in two places: the retriever fetches the wrong documents, or the model ignores the right ones. DeepEval offers metrics for each stage. Pass the retrieved text in the retrieval_context field:
from deepeval import assert_test
from deepeval.test_case import LLMTestCase
from deepeval.metrics import FaithfulnessMetric
def test_answer_stays_grounded():
metric = FaithfulnessMetric(threshold=0.7)
test_case = LLMTestCase(
input="What is the warranty period?",
actual_output="The warranty lasts two years.",
retrieval_context=["All products include a two-year limited warranty."],
)
assert_test(test_case, [metric])
Faithfulness checks whether the answer's claims match the retrieved context, which catches hallucinations — the same failure class our hallucination detection guide catalogs. DeepEval also includes contextual metrics, such as contextual precision, recall, and relevancy, that evaluate the retriever itself. Check the docs for the current list and exact names.
Connect Your Real App
Hardcoded outputs only teach you the syntax. Real tests call your app and score what it produces. Wrap your app in a function, then use it inside the test:
from my_app import answer_question # your RAG pipeline or chatbot
def build_case(question, expected):
answer, context = answer_question(question)
return LLMTestCase(
input=question,
actual_output=answer,
expected_output=expected,
retrieval_context=context,
)
Now every test exercises your actual prompt, model, and retriever. When someone changes any of them, the tests notice.
Run Many Cases With Pytest Parametrization
A suite needs many cases. Pytest's parametrize loops over a list, and each case shows up as its own test:
import pytest
from deepeval import assert_test
from deepeval.metrics import AnswerRelevancyMetric, GEval
CASES = [
("What is your return policy?", "Items can be returned within 30 days."),
("Do you ship internationally?", "Yes, we ship to over 40 countries."),
("How do I reset my password?", "Use the 'Forgot password' link on the login page."),
]
@pytest.mark.parametrize("question,expected", CASES)
def test_support_bot(question, expected):
test_case = build_case(question, expected)
relevancy = AnswerRelevancyMetric(threshold=0.6)
assert_test(test_case, [relevancy])
Build your case list from real user questions and past failures. Each bug you fix should become a new row — the custom eval dataset discipline, applied at test granularity.
Figure 1: The pytest output you already trust — extended with scores from a judge model
Speed It Up With Parallel Runs
LLM-based metrics make network calls, so tests run slowly. The runner can run tests in parallel with the -n flag:
deepeval test run test_chatbot.py -n 4
The docs also describe a -c flag that caches results, which saves tokens when you rerun unchanged tests. Confirm how both flags behave in your installed version.
Set Thresholds Without Guessing
A threshold of 0.5 or 0.7 looks arbitrary because it is. Pick yours from data instead:
- Run your suite on a version of the app you consider good.
- Look at the score distribution for each metric.
- Set the threshold a bit below typical good scores, so normal noise doesn't fail tests.
- Raise it over time as your app improves.
LLM judges vary from run to run. If a test hovers near its threshold, run it several times, or widen your margin — the same variance story behind every judge-based evaluation.
Mix LLM Metrics With Plain Assertions
Don't use a judge for things code can check. Plain pytest assertions run instantly, cost nothing, and never flake:
import json
def test_output_is_valid_json():
output = answer_as_json("List three colors.")
data = json.loads(output) # fails the test if parsing breaks
assert len(data["colors"]) == 3
Use deterministic checks for format, length, required fields, and banned phrases. Save LLM metrics for questions that need judgment — the exact layer split from structured output evaluation.
Run It in CI
Tests only protect you when they run automatically. Add the command to your pipeline, and give it your API key as a secret:
name: llm-tests
on: [pull_request]
jobs:
evals:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pip install -r requirements.txt
- run: deepeval test run tests/ -n 4
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
Keep pull request suites small and fast, and run the full suite nightly or before release.
Control Cost and Flakiness
- Use a smaller judge model for routine runs, and a stronger one for release checks.
- Run a smoke suite of 20 to 50 cases on every pull request.
- Cache and parallelize to save time and tokens.
- Calibrate the judge. Compare its verdicts with human labels on a sample, since judge models carry their own biases.
- Track scores over time, not only pass or fail, so you notice slow drift.
Common Pitfalls
- Testing hardcoded outputs — wire tests to your real app.
- Guessing thresholds — base them on a measured baseline.
- Using judges for everything — plain asserts handle format checks better.
- Ignoring the reasons — the judge's explanation often names the bug.
- Skipping the human check — an uncalibrated judge is a confident opinion.
- Running the full suite on every commit — cost and time balloon.
- Letting flaky judge scores block merges — widen margins near thresholds or retry before failing.
- Never updating the case list — stale suites stop catching new regressions.
Best Book to Read
| Cover | Book | Description | Get it |
|---|---|---|---|
![]() |
Python Testing with pytest | fixtures, parametrization, and the pytest habits DeepEval rides on. | View on Amazon |
![]() |
Unit Testing Principles, Patterns and Practices | what makes a test trustworthy, translated to probabilistic outputs. | View on Amazon |
![]() |
Continuous Delivery | the pipeline discipline that makes a quality gate on every merge possible. | View on Amazon |
![]() |
AI Engineering | evaluation-driven development: metrics, judge calibration, and the feedback loop around your LLM app. | View on Amazon |
As an Amazon Associate I earn from qualifying purchases. Links on this page are affiliate links, and I may earn a commission at no extra cost to you.
Unlock AI That Actually Works
Get lifetime access to GPT-6 Astra, Claude Fable 5.1, Gemini 3.5, Grok 4.5, and more — all in one platform. Build websites, apps, videos, content, and digital products from a single command. No monthly fees. No tool-hopping.
Click here to get GPTAstra Max now — one-time payment, lifetime access.
Frequently Asked Questions
What is DeepEval and how does it fit into pytest?
DeepEval is an open-source Python framework for testing LLM apps that borrows pytest's structure, so a test looks like a normal test: you build a test case, pick metrics, and assert. Three ideas drive it: test cases (LLMTestCase) hold the input, the app's actual output, and optional extras like expected output or retrieved context; metrics score a test case, usually from 0 to 1, and many metrics use an LLM as the judge; thresholds turn a score into pass or fail — a test passes when the score meets the threshold. Install with pip install -U deepeval, set your judge model key (by default LLM-based metrics call an OpenAI model, so export OPENAI_API_KEY; you can also configure another judge through DeepEval's custom model support), create a test file with the test_ prefix that pytest and the DeepEval runner both look for, and run it with deepeval test run test_chatbot.py. You'll see a results table with the test case, the metric, the score, and a pass or fail status — the runner wraps pytest, so familiar behavior carries over. Because many metrics call a judge model, they cost tokens and carry variance; keep that in mind as you set thresholds.
How do you write custom quality criteria with G-Eval?
Built-in metrics cover common needs — AnswerRelevancyMetric checks whether an answer is relevant to the question, and FaithfulnessMetric checks whether the answer's claims match the retrieved context, which catches hallucinations; contextual metrics such as contextual precision, recall, and relevancy evaluate the retriever itself. For your own quality rules, use G-Eval, which lets you describe the criteria in plain English: instantiate GEval with a name, a criteria string (for example: "the actual output must agree with the expected output on all facts and must not add unsupported claims"), evaluation_params drawn from LLMTestCaseParams (INPUT, ACTUAL_OUTPUT, EXPECTED_OUTPUT), and a threshold; then pass it to assert_test with your test case. You can encode tone rules, policy compliance, required disclaimers, or anything else you can describe clearly — it's the most useful metric in the box for product-specific standards. Every LLM-based metric returns a score and a reason that explains the score in plain language: read the reasons, because they show what the judge noticed and often point straight at the fix.
How do you set thresholds without guessing?
Pick thresholds from data: run your suite on a version of the app you consider good, look at the score distribution for each metric, set the threshold a bit below typical good scores so normal noise doesn't fail tests, then raise it over time as your app improves. A threshold of 0.5 or 0.7 looks arbitrary because it is. LLM judges vary run to run: if a test hovers near its threshold, run it several times or widen your margin. To control cost and flakiness, use a smaller judge model for routine runs and a stronger one for release checks; run a smoke suite of 20 to 50 cases on every pull request; cache and parallelize (the runner's -n flag runs tests in parallel, and the docs describe a -c flag that caches results and saves tokens on reruns — confirm both in your installed version); calibrate the judge by comparing its verdicts with human labels on a sample, since judge models carry their own biases; and track scores over time, not only pass or fail, so you notice slow drift.
How do you run DeepEval tests in CI?
Tests only protect you when they run automatically: add deepeval test run tests/ -n 4 to your pipeline and give it your API key as a secret (in GitHub Actions, an env block with secrets.OPENAI_API_KEY). Keep pull request suites small and fast, and run the full suite nightly or before release. Wire tests to your real app instead of hardcoded outputs — wrap your pipeline in a build_case(question, expected) helper that returns an LLMTestCase with actual_output, expected_output, and retrieval_context, so every test exercises your actual prompt, model, and retriever, and when someone changes any of them the tests notice. Parametrize over 30 to 50 real questions with pytest.mark.parametrize, building the case list from real user questions and past failures: each bug you fix should become a new row. Mix LLM metrics with plain assertions — don't use a judge for things code can check: format, length, required fields, and banned phrases are handled by plain pytest asserts that run instantly, cost nothing, and never flake.
Wrapping This Up
DeepEval turns LLM quality checks into tests your team already knows how to run. Start with one relevancy test, add custom G-Eval criteria for your own rules, connect your real app, and let CI enforce the bar.
Write your first test today with three real questions. By the end of the week, you'll catch the next prompt regression before your users do.



