Back to Curriculum

Testing with JUnit

📚 Lesson 12 of 16 ⏱️ 50 min

Testing with JUnit

50 min

Testing is essential for building reliable Java applications. Tests verify that code works correctly, prevent regressions, enable confident refactoring, and serve as documentation. JUnit is the de facto standard testing framework for Java, providing annotations, assertions, test organization, and lifecycle management. Understanding testing helps you build applications with confidence and maintain code quality over time.

JUnit 5 (Jupiter) is the current version, providing annotations like `@Test`, `@BeforeEach`, `@AfterEach`, `@BeforeAll`, `@AfterAll`, and `@DisplayName`. Assertions verify expected behavior: `assertEquals()`, `assertTrue()`, `assertNotNull()`, `assertThrows()`, and more. Test methods are organized into test classes. Understanding JUnit annotations and assertions enables you to write effective tests.

Test-driven development (TDD) involves writing tests before implementing functionality. The TDD cycle is: write a failing test, implement minimal code to pass, refactor. TDD helps you think about requirements, design better APIs, and maintain high test coverage. While TDD isn't always practical, understanding it helps you write testable code and maintain quality. Understanding TDD helps you develop with confidence.

Mocking frameworks like Mockito enable you to create fake objects (mocks) for testing. Mocks isolate units under test by replacing dependencies with controllable fake implementations. You can verify method calls, return specific values, and throw exceptions. Mocking is essential for testing classes with external dependencies (databases, web services, file systems). Understanding mocking helps you write isolated, fast tests.

Different test types serve different purposes. Unit tests test individual units in isolation. Integration tests test how multiple units work together. End-to-end tests test complete application workflows. Each type provides different confidence levels and catches different issues. Understanding test types helps you build comprehensive test coverage that catches bugs at appropriate levels.

Best practices include writing tests before or alongside code, testing behavior not implementation, keeping tests simple and focused, using descriptive test names, maintaining high test coverage, and running tests frequently. Tests should be fast, reliable, and maintainable. Understanding testing enables you to build robust Java applications with confidence.

Key Concepts

  • Testing verifies code works correctly and prevents regressions.
  • JUnit is the standard testing framework for Java.
  • TDD involves writing tests before implementing functionality.
  • Mocking frameworks help isolate units under test.
  • Different test types (unit, integration, E2E) serve different purposes.

Learning Objectives

Master

  • Writing unit tests with JUnit
  • Using JUnit annotations and assertions
  • Working with mocking frameworks (Mockito)
  • Understanding TDD and test organization

Develop

  • Test-driven development thinking
  • Understanding testing strategies and coverage
  • Designing testable, maintainable code

Tips

  • Write tests before or alongside code (TDD approach).
  • Test behavior, not implementation details.
  • Use Mockito to mock dependencies and isolate units.
  • Keep tests simple, focused, and maintainable.

Common Pitfalls

  • Not writing tests, making refactoring risky.
  • Testing implementation details instead of behavior.
  • Not using mocks, making tests slow and brittle.
  • Writing tests that are too complex, making them hard to maintain.

Summary

  • Testing is essential for reliable Java applications.
  • JUnit provides comprehensive testing framework.
  • TDD helps design better code and maintain quality.
  • Mocking frameworks enable isolated unit testing.
  • Understanding testing enables confident development.

Exercise

Write comprehensive unit tests for a Calculator class using JUnit and Mockito.

import org.junit.jupiter.api.*;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

class Calculator {
    public int add(int a, int b) {
        return a + b;
    }
    
    public int subtract(int a, int b) {
        return a - b;
    }
    
    public int multiply(int a, int b) {
        return a * b;
    }
    
    public double divide(int a, int b) {
        if (b == 0) {
            throw new IllegalArgumentException("Cannot divide by zero");
        }
        return (double) a / b;
    }
}

class CalculatorTest {
    private Calculator calculator;
    
    @BeforeEach
    void setUp() {
        calculator = new Calculator();
    }
    
    @Test
    @DisplayName("Addition should work correctly")
    void testAddition() {
        assertEquals(4, calculator.add(2, 2));
        assertEquals(0, calculator.add(-1, 1));
        assertEquals(100, calculator.add(50, 50));
    }
    
    @Test
    @DisplayName("Subtraction should work correctly")
    void testSubtraction() {
        assertEquals(0, calculator.subtract(2, 2));
        assertEquals(-2, calculator.subtract(-1, 1));
        assertEquals(25, calculator.subtract(50, 25));
    }
    
    @ParameterizedTest
    @ValueSource(ints = {1, 2, 3, 4, 5})
    @DisplayName("Multiplication by zero should return zero")
    void testMultiplicationByZero(int number) {
        assertEquals(0, calculator.multiply(number, 0));
    }
    
    @Test
    @DisplayName("Division by zero should throw exception")
    void testDivisionByZero() {
        assertThrows(IllegalArgumentException.class, () -> {
            calculator.divide(10, 0);
        });
    }
    
    @Test
    @DisplayName("Division should work correctly")
    void testDivision() {
        assertEquals(2.5, calculator.divide(5, 2));
        assertEquals(0.5, calculator.divide(1, 2));
    }
    
    @Test
    @DisplayName("Calculator should handle negative numbers")
    void testNegativeNumbers() {
        assertEquals(-4, calculator.add(-2, -2));
        assertEquals(0, calculator.subtract(-5, -5));
        assertEquals(10, calculator.multiply(-2, -5));
    }
}

Code Editor

Output