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
- One behaviour per test — a test should answer a single question. If that needs several Assert statements, use them; just avoid unrelated checks.
- AAA with a Baseline — because Arrange is split (constructor / helper / test), keep one “defaults pass” test to catch drift early.
- 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.
- 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. - 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.