Codementor Events

Unit‑Test Code Is Still Code — Apply the Same Standards

Published Apr 22, 2025
Unit‑Test Code Is Still Code — Apply the Same Standards

Arrange ‑ Act ‑ Assert (AAA)

AAA is the mental checklist for every unit test:

  • Arrange – prepare the Subject Under Test (SUT) and its dependencies
  • Act – call the method you want to test
  • Assert – verify the outcome

In most well‑written tests the Act step is a single line. Assert varies (the “one‑assertion” discussion is for another day). The biggest surface area is usually Arrange: a few lines for simple cases, but dozens when mocks need precise behaviour.

We apply DRY (Don’t Repeat Yourself) in production code, yet test setup is often copied verbatim “because tests must be independent”. There is a middle ground.

Why This Matters

Copy‑paste tests rot quickly. When a constructor changes or a new dependency is added, you fix the same problem in a dozen places and still miss one. Treat test code like production code and the suite becomes an asset instead of overhead.


The examples below use simplified C# with xUnit for illustration, but the same ideas apply to TypeScript + Jasmine, Python + pytest, Java + JUnit, and most other mainstream test frameworks.

Problem Example — Duplication Hides Intent

public class UnitTest1
{
    [Fact]
    public void Returns_2_when_Dependency1_returns_2()
    {
        var dependency1 = new Mock<IDependency1>();
        var dependency2 = new Dependency2();
        dependency1.Setup(x => x.Call(1)).Returns(2);

        var sut = new MyTestClass(dependency1.Object, dependency2);

        sut.Execute().Should().Be(2);
    }

    [Fact]
    public void Returns_3_when_Dependency1_returns_3()
    {
        var dependency1 = new Mock<IDependency1>();
        var dependency2 = new Dependency2();
        dependency1.Setup(x => x.Call(1)).Returns(3);

        var sut = new MyTestClass(dependency1.Object, dependency2);

        sut.Execute().Should().Be(3);
    }

    [Fact]
    public void Returns_minus4_with_alternative_strategy_and_2()
    {
        var dependency1 = new Mock<IDependency1>();
        var dependency2 = new Dependency2Alt();
        dependency1.Setup(x => x.Call(1)).Returns(2);

        var sut = new MyTestClass(dependency1.Object, dependency2);

        sut.Execute().Should().Be(-4);
    }

    [Fact]
    public void Returns_minus6_with_alternative_strategy_and_3()
    {
        var dependency1 = new Mock<IDependency1>();
        var dependency2 = new Dependency2Alt();
        dependency1.Setup(x => x.Call(1)).Returns(3);

        var sut = new MyTestClass(dependency1.Object, dependency2);

        sut.Execute().Should().Be(-6);
    }
}

The signal (what is different) is buried in the noise (what repeats). Imagine four dependencies instead of two—will you spot the tiny delta?


Refactored Example — Baseline + Targeted Overrides

public class UnitTest1
{
    // Shared fixture
    private readonly Mock<IDependency1> dependency1;
    private IDependency2               dependency2;

    public UnitTest1()
    {
        dependency1 = new Mock<IDependency1>();
        dependency1.Setup(x => x.Call(1)).Returns(2);      
        dependency2 = new Dependency2();                   
    }

    private int Run() => new MyTestClass(dependency1.Object, dependency2).Execute();

    [Fact]
    public void Defaults_are_valid() => Run().Should().Be(2);

    [Fact]
    public void Returns_3_when_Dependency1_changes()
    {
        dependency1.Setup(x => x.Call(1)).Returns(3);
        Run().Should().Be(3);
    }

    [Fact]
    public void Returns_minus4_with_alternative_strategy()
    {
        dependency2 = new Dependency2Alt();
        Run().Should().Be(-4);
    }

    [Fact]
    public void Returns_minus6_with_alternative_strategy_and_3()
    {
        dependency2 = new Dependency2Alt();
        dependency1.Setup(x => x.Call(1)).Returns(3);
        Run().Should().Be(-6);
    }
}

Each test highlights only the variation. A constructor change is fixed once; a new default mock behaviour is set once.


Is This “The Best” Way?

No single style fits every codebase. Some examples that work well with this approach:

  • Move Arrange into helper methods that read like prose (SwitchToAltStrategy())
  • Use fixture builders when the setup itself becomes complex
  • Split into multiple small test classes when behaviour branches widely

Start simple, adapt to your team’s standards, and keep readability front‑and‑centre.


Five Pragmatic Guidelines

  1. One behaviour per test — a test should answer a single question. If that needs several Assert statements, use them; just avoid unrelated checks.
  2. AAA with a Baseline — because Arrange is split (constructor / helper / test), keep one “defaults pass” test to catch drift early.
  3. Only relevant overrides — minimise changes from the baseline fixture. Prefer one‑line tweaks, but two or three are fine when truly required. Frequent multi‑line overrides are a sign to refactor.
  4. No conditionals in Arrange — branching logic (if/else) inside test setup is a smell. Fork the test or move complexity to helpers that have their own tests.
  5. Stay independent — each test must succeed in isolation. Know your runner’s lifecycle (xUnit creates a fresh class instance per test).

Final Thought

A disciplined test suite is cheap to maintain and hard to break. Apply the same design principles you expect in production code—just in smaller, sharper strokes.

Discover and read more posts from Patrick Verbeeten
get started