Unit Testing Best Practices

Unit tests verify the smallest testable parts of an application in isolation. High-quality unit tests act as executable documentation and provide a safety net for refactoring.

The FIRST Principles

- **Fast:** Tests should run in milliseconds so they can be executed after every code change.

- **Isolated:** Tests should not depend on each other or external systems (DB, Network).

- **Repeatable:** Running the test 100 times should yield the same result.

- **Self-Validating:** No manual interpretation of results; it passes or fails.

- **Thorough:** Cover edge cases, nulls, and boundary conditions, not just the "happy path."

Anatomy of a Good Test

Use the **Arrange-Act-Assert** (AAA) pattern to keep tests readable.

```java

@Test

void shouldCalculateDiscountForPremiumUser() {

// Arrange

User user = new User("Alice", UserTier.PREMIUM);

PricingEngine engine = new PricingEngine();

// Act

double price = engine.calculatePrice(100.0, user);

// Assert

assertEquals(80.0, price, "Premium users should get a 20% discount");

}

```

Table-Driven Tests (Parameterized)

Instead of writing five tests for five different inputs, use parameterized tests to map inputs to expected outputs. This is the standard for complex logic.

```java

@ParameterizedTest

@CsvSource({

"10, 2.0",

"50, 10.0",

"100, 20.0",

"0, 0.0"

})

void shouldCalculateCorrectTax(double amount, double expectedTax) {

TaxCalculator calc = new TaxCalculator(0.20);

assertEquals(expectedTax, calc.calculate(amount));

}

```

Mocking and Boundaries

Use mocks (e.g., Mockito) only for **external boundaries** or complex dependencies you don't control. Do not mock internal logic or data objects (POJOs/Records).

- **Good Mock:** Mocking a `PaymentGateway` that hits a 3rd party API.

- **Bad Mock:** Mocking a `List` or a simple `User` object.

Common Pitfalls

1. **Testing Implementation, Not Behavior:** If you rename a private method and the test breaks, your test is too brittle. Assert on the output, not the internal calls.

2. **The "Slow Unit Test" Oxymoron:** If a test hits a database or starts a Spring context, it is an **Integration Test**, not a Unit Test. Move it to the appropriate suite.

3. **Over-Mocking:** If your test setup is 50 lines of `when(...).thenReturn(...)` for a 5-line method, your class probably has too many responsibilities (SRP violation).

4. **Assertion Roulette:** Multiple assertions in one test without clear messages. If it fails, you won't know which one failed without a debugger.

Verification Checklist

- [ ] Does the test fail if I break the code? (Verify the test is actually testing something).

- [ ] Is there a clear "Reason for Failure" in the assertion message?

- [ ] Are boundary conditions (0, -1, null, max_int) covered?

- [ ] Can I run this test without internet access or a database?