Approval Testing with the ApprovalTests Library
The ApprovalTests library is the canonical implementation of approval testing, available across Java, .NET, JavaScript, Python, Ruby, and PHP. It handles the mechanics of capturing output, managing approved/received files, and opening diff tools—leaving you to focus on what to test.
This guide covers practical usage across the three most common environments: Java, .NET, and JavaScript.
How ApprovalTests Works
Every ApprovalTests test follows the same pattern:
- Call
verify(someOutput)in your test - The library writes the output to a
.received.txtfile - It compares the received file to the
.approved.txtfile (if it exists) - Match: test passes, received file is deleted
- No match or no approved file: test fails, diff tool opens
When you approve output, you accept the received file as correct—it becomes (or overwrites) the approved file.
Java Setup and Usage
Add the dependency
<!-- Maven -->
<dependency>
<groupId>com.approvaltests</groupId>
<artifactId>approvaltests</artifactId>
<version>22.3.3</version>
<scope>test</scope>
</dependency>Basic usage with JUnit 5
import com.approvaltests.Approvals;
import org.junit.jupiter.api.Test;
class InvoiceTest {
@Test
void generatesInvoiceSummary() {
Invoice invoice = new Invoice(
new Customer("Alice", "alice@example.com"),
List.of(new LineItem("Widget", 2, 49.99)),
LocalDate.of(2025, 2, 15)
);
String summary = invoice.toFormattedText();
Approvals.verify(summary);
// Creates: InvoiceTest.generatesInvoiceSummary.approved.txt
}
}First run output (received file):
Customer: Alice (alice@example.com)
Invoice Date: 2025-02-15
Due Date: 2025-03-15
Items:
Widget x2 @ $49.99 = $99.98
Total: $99.98Review it. If correct: rename received to approved.
# Approve manually
mv "InvoiceTest.generatesInvoiceSummary.received.txt" \
"InvoiceTest.generatesInvoiceSummary.approved.txt"Or use the diff tool that opens automatically and accept the changes.
Approving combinations of inputs
ApprovalTests has a powerful combinatorial testing feature:
@Test
void priceAtAllTierLevels() {
String[] tiers = { "basic", "pro", "enterprise" };
Integer[] quantities = { 1, 10, 100 };
Approvals.verifyAllCombinations(
(tier, qty) -> String.format("Tier=%s, Qty=%d → $%.2f",
tier, qty, pricingService.calculate(tier, qty)),
tiers,
quantities
);
}This automatically generates and verifies a table of all 9 combinations:
(basic, 1) => Tier=basic, Qty=1 → $9.99
(basic, 10) => Tier=basic, Qty=10 → $89.91
(basic, 100) => Tier=basic, Qty=100 → $799.00
(pro, 1) => Tier=pro, Qty=1 → $19.99
...One approved file covers all combinations. If pricing logic changes, the diff shows exactly which combinations changed.
.NET Setup and Usage
NuGet install
dotnet add package ApprovalTestsBasic usage with NUnit
using ApprovalTests;
using ApprovalTests.Reporters;
using NUnit.Framework;
[TestFixture]
[UseReporter(typeof(DiffReporter))]
public class ReportGeneratorTests
{
[Test]
public void GeneratesMonthlyReport()
{
var data = new SalesData
{
Month = new DateTime(2025, 1, 1),
Entries = new[] {
new SaleEntry("Widget", 50, 49.99m),
new SaleEntry("Gadget", 20, 99.99m)
}
};
string report = ReportGenerator.GenerateMonthly(data);
Approvals.Verify(report);
}
}Configuring reporters
The reporter controls what happens when a test fails—which diff tool opens.
// Global: set in AssemblyInfo or static initializer
[assembly: UseReporter(typeof(DiffReporter))]
// Per-test class
[UseReporter(typeof(WinMergeReporter))]
public class MyTests { ... }
// Per-test (useful in CI)
[UseReporter(typeof(QuietReporter))] // Never opens a GUI
[Test]
public void MyTest() { ... }Built-in reporters: DiffReporter, WinMergeReporter, KaleidoscopeReporter, VSCodeReporter, FileLauncherReporter, QuietReporter (CI-safe).
Verifying objects
// Serialize to JSON and verify
var user = userService.CreateUser("Bob", "bob@example.com");
Approvals.VerifyJson(JsonSerializer.Serialize(user, prettyOptions));
// Verify XML
Approvals.VerifyXml(xmlDocument.ToString());
// Verify HTML
Approvals.VerifyHtml(htmlContent);JavaScript Setup and Usage
Install
npm install --save-dev approvalsUsage with Jest
// jest.config.js
module.exports = {
setupFilesAfterFramework: ["approvals/lib/Providers/Jest/register"]
};import { verify } from "approvals/lib/Providers/Jest/JestApprovals";
describe("OrderSummary", () => {
test("formats order correctly", () => {
const order = {
id: "ORD-001",
customer: "Alice",
items: [
{ name: "Widget", qty: 2, price: 49.99 },
{ name: "Gadget", qty: 1, price: 99.99 }
]
};
const summary = formatOrderSummary(order);
verify(summary);
});
});Configuring in JavaScript
// approvals.config.js
const { configure } = require("approvals");
const { VSCodeReporter } = require("approvals/lib/Reporters/VSCodeReporter");
configure({
reporters: [new VSCodeReporter()],
normalizeLineEndingsTo: "\n",
appendEOL: true,
errorOnStaleApprovedFiles: true // fail if approved file has no matching test
});Managing Approved Files
File naming convention
ApprovalTests names files based on the test class and method:
{TestClassName}.{MethodName}.approved.txt
{TestClassName}.{MethodName}.received.txtIn Java/JUnit: files go next to the test class by default.
In .NET: files go in the project directory.
In JavaScript: files go in a __approved__ directory next to the test file.
Committing to version control
Always commit approved files. They're your expected behavior documentation.
# .gitignore
*.received.txt # Never commit received files
# *.approved.txt # DO commit approved filesBatch approval (CLI)
ApprovalTests for .NET includes an approval CLI:
dotnet tool install -g ApprovalTests.CLI
# Review and approve all failing tests
approvals review
# Auto-approve all (use carefully—bypasses review)
approvals approve-allCI/CD Configuration
In CI, you don't want diff tools opening GUIs. Configure a quiet reporter:
Java:
@ExtendWith(ApprovalExtension.class)
class MyTest {
@Test
void myTest() {
// CI_APPROVED_FILES env var or system property controls behavior
Approvals.verify(output);
}
}Set APPROVAL_TESTS_USE_REPORTER=ClipboardReporter or configure QuietReporter in CI.
.NET:
# In CI environment variable
APPROVALTESTS_QUIET=trueJavaScript:
// In CI (NODE_ENV=ci or CI=true), approvals won't open reporters
configure({
reporters: process.env.CI ? [] : [new VSCodeReporter()]
});In CI, a missing or mismatched approved file always fails the test—no reporter interaction needed.
Handling Volatile Data
// Normalize timestamps before verifying
function normalizeForApproval(output) {
return output
.replace(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/g, "[TIMESTAMP]")
.replace(/[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/gi, "[UUID]");
}
test("creates user with approval", () => {
const user = userService.create({ name: "Alice" });
verify(normalizeForApproval(JSON.stringify(user, null, 2)));
});Beyond the Library
ApprovalTests handles the mechanics. It doesn't tell you what to approve test. Good targets:
- Complex return values with many fields
- Generated text (reports, emails, CSV exports)
- Serialized objects before and after transformations
- API response structures
- SQL queries generated by ORMs
- Error message formatting
End-to-End Coverage
ApprovalTests verifies function output—not browser behavior, UI rendering, or real user flows.
HelpMeTest complements approval testing with plain-English end-to-end tests against your live application. When your approved files confirm the report generator produces the right text, HelpMeTest confirms the report downloads correctly in the actual browser.
Summary
- ApprovalTests provides
verify()which captures output to.received.txtand compares to.approved.txt - Approved files must be committed to version control
- Configure reporters per environment—GUI reporters for development, quiet reporters for CI
- Use combinatorial testing for covering multiple input combinations in one approved file
- Normalize volatile data (timestamps, IDs) before verifying
- Available in Java, .NET, JavaScript, Python, Ruby with consistent behavior across languages