Test Coverage

Why Testing Matters

Tests are the safety net of your codebase. Without them, every change is a gamble — you push code and hope nothing breaks. With a solid test suite, you refactor with confidence, onboard new developers faster, and catch regressions before they reach production. In professional engineering teams, untested code is considered unfinished code.

The Test Pyramid

Not all tests are equal. The test pyramid gives you a practical strategy for where to invest your testing effort:

  • Unit Tests (base) — Fast, isolated, test a single function or class. You should have the most of these.
  • Integration Tests (middle) — Verify that components work together (e.g., service + database, API calls).
  • End-to-End Tests (top) — Simulate real user workflows. Slow and brittle, so keep these minimal.

The rule of thumb: if a bug can be caught by a unit test, don’t write an integration test for it. Push tests as far down the pyramid as possible.

Unit Testing — Java (JUnit 5)

Unit tests verify a single piece of logic in isolation. Here is a straightforward example using JUnit 5:

public class PriceCalculator {
    public double applyDiscount(double price, double discountPercent) {
        if (discountPercent < 0 || discountPercent > 100) {
            throw new IllegalArgumentException("Invalid discount");
        }
        return price - (price * discountPercent / 100);
    }
}

// Test class
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

class PriceCalculatorTest {

    private final PriceCalculator calculator = new PriceCalculator();

    @Test
    void shouldApplyTenPercentDiscount() {
        double result = calculator.applyDiscount(100.0, 10);
        assertEquals(90.0, result);
    }

    @Test
    void shouldThrowForNegativeDiscount() {
        assertThrows(IllegalArgumentException.class,
            () -> calculator.applyDiscount(100.0, -5));
    }
}

Unit Testing — Python (pytest)

The same concept in Python using pytest:

class PriceCalculator:
    def apply_discount(self, price: float, discount_percent: float) -> float:
        if discount_percent < 0 or discount_percent > 100:
            raise ValueError("Invalid discount")
        return price - (price * discount_percent / 100)

# test_price_calculator.py
import pytest
from price_calculator import PriceCalculator

calculator = PriceCalculator()

def test_should_apply_ten_percent_discount():
    result = calculator.apply_discount(100.0, 10)
    assert result == 90.0

def test_should_throw_for_negative_discount():
    with pytest.raises(ValueError):
        calculator.apply_discount(100.0, -5)

Integration Testing

Integration tests verify that multiple components work together correctly. Common scenarios include testing a service layer that talks to a database, or an API endpoint that coordinates multiple services.

@SpringBootTest
@AutoConfigureMockMvc
class OrderControllerIntegrationTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    void shouldCreateOrderAndReturnCreatedStatus() throws Exception {
        String orderJson = "{\"item\": \"Laptop\", \"quantity\": 1}";

        mockMvc.perform(post("/api/orders")
                .contentType(MediaType.APPLICATION_JSON)
                .content(orderJson))
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.item").value("Laptop"));
    }
}

The key difference: unit tests mock everything external, integration tests let real components interact.

What Makes a Good Test

Follow the Arrange-Act-Assert pattern in every test:

  • Arrange — Set up the data and dependencies.
  • Act — Execute the behavior under test.
  • Assert — Verify the expected outcome.

Other principles that matter:

  • Descriptive namesshouldReturnZeroWhenCartIsEmpty() beats test1().
  • One assertion per concept — Test one behavior at a time.
  • Independent tests — No test should depend on another test running first.
  • Fast execution — Slow tests get skipped. Keep unit tests under milliseconds.

Code Coverage — Aim for Meaningful, Not 100%

Code coverage measures what percentage of your code is exercised by tests. Tools like JaCoCo (Java) and coverage.py (Python) generate these reports.

A reasonable target is 70-80% line coverage. Chasing 100% leads to brittle tests on trivial code (getters, setters, boilerplate). Focus your testing effort on:

  • Business logic and calculations
  • Edge cases and error handling
  • Code that changes frequently
  • Anything that previously had a bug

Coverage tells you what is not tested. It does not tell you if your tests are any good. A test with no assertions still counts as coverage.

Mocking Dependencies

When your code depends on external services, databases, or APIs, you mock those dependencies so your test stays fast and isolated.

Java — Mockito:

@ExtendWith(MockitoExtension.class)
class OrderServiceTest {

    @Mock
    private PaymentGateway paymentGateway;

    @InjectMocks
    private OrderService orderService;

    @Test
    void shouldCompleteOrderWhenPaymentSucceeds() {
        // Arrange
        when(paymentGateway.charge(anyDouble())).thenReturn(true);

        // Act
        boolean result = orderService.placeOrder(new Order("Laptop", 999.99));

        // Assert
        assertTrue(result);
        verify(paymentGateway).charge(999.99);
    }
}

Python — unittest.mock:

from unittest.mock import Mock, patch
from order_service import OrderService

def test_should_complete_order_when_payment_succeeds():
    # Arrange
    mock_gateway = Mock()
    mock_gateway.charge.return_value = True
    service = OrderService(payment_gateway=mock_gateway)

    # Act
    result = service.place_order({"item": "Laptop", "price": 999.99})

    # Assert
    assert result is True
    mock_gateway.charge.assert_called_once_with(999.99)

Senior Tip: Test Behavior, Not Implementation

The biggest mistake developers make is testing how code works instead of what it does. If you refactor internals and your tests break — even though behavior is unchanged — your tests are too tightly coupled to implementation.

Bad: Asserting that a specific private method was called three times.

Good: Asserting that given input X, the output is Y.

Your tests should answer one question: “Does this code produce the correct result for the given input?” If you can swap out the implementation and your tests still pass, you have written good tests.

Invest in testing early. The cost of writing tests is always less than the cost of debugging production at 2 AM.




Subscribe To Our Newsletter
You will receive our latest post and tutorial.
Thank you for subscribing!

required
required


Leave a Reply

Your email address will not be published. Required fields are marked *