Java > Testing in Java > Test-Driven Development (TDD) > Writing Tests First

TDD Example: Simple Calculator

This example demonstrates Test-Driven Development (TDD) by building a simple calculator with addition functionality. We'll write the test first, watch it fail, implement the functionality, and then watch the test pass. This process ensures that our code is testable and meets the defined requirements.

1. Defining the Requirement

Our requirement is to create a Calculator class with an add method that takes two integers as input and returns their sum.

2. Writing the Test First

We create a CalculatorTest class using JUnit 5. This class contains a testAddTwoPositiveNumbers method that instantiates a Calculator, calls the add method with 2 and 3, and asserts that the result is 5. We also include tests for adding positive and negative numbers and adding two negative numbers. Note that the Calculator class does not yet exist, so this test will initially fail to compile or run.

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;

public class CalculatorTest {

    @Test
    void testAddTwoPositiveNumbers() {
        Calculator calculator = new Calculator();
        int result = calculator.add(2, 3);
        assertEquals(5, result, "The sum of 2 and 3 should be 5");
    }

    @Test
    void testAddOnePositiveAndOneNegativeNumber() {
        Calculator calculator = new Calculator();
        int result = calculator.add(5, -2);
        assertEquals(3, result, "The sum of 5 and -2 should be 3");
    }

    @Test
    void testAddTwoNegativeNumbers() {
       Calculator calculator = new Calculator();
       int result = calculator.add(-1, -1);
       assertEquals(-2, result, "The sum of -1 and -1 should be -2");
    }
}

3. Running the Test (Expect Failure)

At this stage, if you attempt to run the test, it will fail (likely with a compilation error) because the Calculator class and the add method do not exist. This is expected in TDD – we're confirming that the test fails when the functionality is missing.

4. Implementing the Functionality

We create the Calculator class with the add method that returns the sum of the two input integers. This is the simplest implementation that satisfies the test.

public class Calculator {
    public int add(int a, int b) {
        return a + b;
    }
}

5. Running the Test (Expect Success)

Now, when you run the CalculatorTest class, all tests should pass. This confirms that the Calculator class and its add method are functioning as expected.

6. Refactoring (Optional)

If necessary, refactor the code to improve its readability, maintainability, or performance. In this simple example, refactoring might not be necessary, but in more complex scenarios, it's an important step.

Concepts Behind the Snippet

This snippet demonstrates the core TDD cycle: Red (Write a failing test), Green (Make the test pass), Refactor (Improve the code). It emphasizes writing tests before writing the actual code, which promotes better design and testability.

Real-Life Use Case

Imagine you are building a feature that handles user authentication. Following TDD, you would first write tests that define the success and failure scenarios (e.g., successful login, invalid password, inactive account) before writing the code that performs the authentication. This ensures that your authentication logic is thoroughly tested and meets all defined requirements.

Best Practices

  • Write small, focused tests that test a single aspect of the functionality.
  • Keep your tests independent of each other.
  • Run your tests frequently.
  • Use descriptive names for your test methods.

Interview Tip

Be prepared to explain the TDD cycle (Red-Green-Refactor) and its benefits. Also, be ready to discuss scenarios where TDD might be more or less suitable.

When to Use Them

TDD is particularly beneficial when you have well-defined requirements and want to ensure that your code is highly testable. It's also helpful for complex or critical systems where thorough testing is essential. TDD can be time-consuming, so it may not be appropriate for simple, throwaway projects.

Memory Footprint

The memory footprint of tests themselves is generally small and is reclaimed when the tests complete. However, poorly written tests, especially those that create large objects or databases, can have a significant memory impact. It's important to write efficient tests to avoid memory leaks and performance issues.

Alternatives

Alternative testing approaches include Behavior-Driven Development (BDD), which focuses on describing the behavior of the system, and traditional testing approaches where tests are written after the code. While BDD is a variation of TDD, the traditional method often leads to less testable code and can miss edge cases.

Pros

  • Improved code quality and testability
  • Reduced bugs and defects
  • Better design and architecture
  • Increased confidence in the code
  • Living documentation of the system's behavior

Cons

  • Can be time-consuming
  • Requires a shift in mindset
  • Can be difficult to apply to legacy code
  • May require more upfront planning

FAQ

  • What is the difference between TDD and traditional testing?

    In TDD, you write tests before you write the code, while in traditional testing, you write tests after the code is written. TDD encourages a more testable design and helps prevent bugs early in the development process.
  • Is TDD always the best approach?

    No, TDD is not always the best approach. It can be time-consuming, and it might not be suitable for simple projects or legacy code. However, it's highly beneficial for complex or critical systems where thorough testing is essential.