Approval Testing with the ApprovalTests Library

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:

  1. Call verify(someOutput) in your test
  2. The library writes the output to a .received.txt file
  3. It compares the received file to the .approved.txt file (if it exists)
  4. Match: test passes, received file is deleted
  5. 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.98

Review 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 ApprovalTests

Basic 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 approvals

Usage 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.txt

In 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 files

Batch 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-all

CI/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=true

JavaScript:

// 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.txt and 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

Read more

Start now free