How do I avoid common mistakes when writing JUnit tests?

Writing JUnit tests is not only about checking whether the code works. Good tests should be clear, reliable, independent, and easy to maintain.

Here are common mistakes beginners often make when writing JUnit tests, along with better ways to avoid them.


1. Do Not Let Tests Depend on Each Other

Each test should be able to run:

  • By itself
  • In any order
  • Repeatedly
  • Together with other tests

Bad Example

import org.junit.jupiter.api.Test;

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

class CounterTest {

    private int counter = 0;

    @Test
    void firstTest() {
        counter = 10;
    }

    @Test
    void secondTest() {
        assertEquals(10, counter);
    }
}

This is a bad test because secondTest() depends on firstTest() running first.

JUnit does not guarantee that tests will run in the order you expect unless you explicitly configure ordering. Even then, relying on test order usually makes tests fragile.

Better Example

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

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

class CounterTest {

    private int counter;

    @BeforeEach
    void setUp() {
        counter = 10;
    }

    @Test
    void firstTest() {
        assertEquals(10, counter);
    }

    @Test
    void secondTest() {
        assertEquals(10, counter);
    }
}

Each test now gets the state it needs before it runs.


2. Use Clear Test Names

A test name should describe the behavior being tested.

Avoid Names Like This

@Test
void test1() {
}
@Test
void testAdd() {
}

These names do not clearly explain what the test verifies.

Prefer Descriptive Names

@Test
void addReturnsSumOfTwoPositiveNumbers() {
}
@Test
void withdrawThrowsExceptionWhenBalanceIsInsufficient() {
}

Good test names make failures easier to understand.


3. Follow the Arrange, Act, Assert Pattern

A clean test usually has three parts:

Step Purpose
Arrange Prepare objects, inputs, and expected values
Act Call the method being tested
Assert Check the result

Example:

import org.junit.jupiter.api.Test;

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

class CalculatorTest {

    @Test
    void addReturnsSumOfTwoNumbers() {
        // Arrange
        Calculator calculator = new Calculator();

        // Act
        int result = calculator.add(2, 3);

        // Assert
        assertEquals(5, result);
    }
}

This structure makes the test easier to read.


4. Do Not Put Too Much Logic in Tests

Tests should be simple. Avoid loops, complex conditions, calculations, or duplicated business logic inside test methods.

Avoid

@Test
void calculatesDiscount() {
    double price = 100;
    double discount = 0.2;
    double expected = price - (price * discount);

    assertEquals(expected, discountService.applyDiscount(price, discount));
}

This test repeats the calculation logic. If the test logic is wrong in the same way as the production code, the test may still pass.

Prefer

@Test
void appliesTwentyPercentDiscount() {
    double result = discountService.applyDiscount(100, 0.2);

    assertEquals(80, result);
}

Use simple, explicit expected values whenever possible.


5. Use the Correct Assertion

JUnit provides many assertions. Choose the one that best describes what you are checking.

assertEquals(expected, actual);
assertTrue(condition);
assertFalse(condition);
assertNull(value);
assertNotNull(value);
assertThrows(ExceptionType.class, () -> methodCall());

Avoid This

assertTrue(result == 5);

Prefer This

assertEquals(5, result);

assertEquals() gives a clearer failure message because JUnit can show the expected and actual values.


6. Remember the Order of assertEquals()

The usual order is:

assertEquals(expected, actual);

Example:

assertEquals(5, calculator.add(2, 3));

Avoid reversing the values:

assertEquals(calculator.add(2, 3), 5);

The test may still work, but the failure message becomes confusing.


7. Test Exceptions Correctly

When testing exceptions, put the code that should throw the exception inside the assertThrows() lambda.

Wrong

@Test
void divideThrowsExceptionWhenDividerIsZero() {
    calculator.divide(10, 0);

    assertThrows(IllegalArgumentException.class, () -> {
    });
}

The exception is thrown before JUnit can check it.

Correct

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertThrows;

class CalculatorTest {

    private final Calculator calculator = new Calculator();

    @Test
    void divideThrowsExceptionWhenDividerIsZero() {
        assertThrows(
                IllegalArgumentException.class,
                () -> calculator.divide(10, 0)
        );
    }
}

You can also check the exception message:

import org.junit.jupiter.api.Test;

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

class CalculatorTest {

    private final Calculator calculator = new Calculator();

    @Test
    void divideThrowsExceptionWhenDividerIsZero() {
        IllegalArgumentException exception = assertThrows(
                IllegalArgumentException.class,
                () -> calculator.divide(10, 0)
        );

        assertEquals("Divider cannot be zero", exception.getMessage());
    }
}

8. Avoid Testing Too Many Things in One Test

A test should usually verify one behavior.

Avoid

@Test
void userTest() {
    User user = new User("Alice", 25);

    assertEquals("Alice", user.getName());
    assertEquals(25, user.getAge());

    user.setAge(26);

    assertEquals(26, user.getAge());
}

This test checks object creation and updating age in the same method.

Prefer

@Test
void constructorSetsUserName() {
    User user = new User("Alice", 25);

    assertEquals("Alice", user.getName());
}

@Test
void constructorSetsUserAge() {
    User user = new User("Alice", 25);

    assertEquals(25, user.getAge());
}

@Test
void setAgeUpdatesUserAge() {
    User user = new User("Alice", 25);

    user.setAge(26);

    assertEquals(26, user.getAge());
}

Smaller tests are easier to understand and easier to debug.


9. Use assertAll() for Related Independent Assertions

If you are checking several independent properties of the same object, assertAll() can be useful.

import org.junit.jupiter.api.Test;

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

class UserTest {

    @Test
    void constructorSetsUserDetails() {
        User user = new User("Alice", 25, "[email protected]");

        assertAll("User details",
                () -> assertEquals("Alice", user.getName()),
                () -> assertEquals(25, user.getAge()),
                () -> assertEquals("[email protected]", user.getEmail())
        );
    }
}

This lets JUnit report multiple failures together instead of stopping at the first failed assertion.

However, only group assertions that are independent. If one assertion must pass before another is safe to run, keep them separate.


10. Do Not Share Mutable State Between Tests

Shared mutable state can make tests unreliable.

Avoid

import java.util.ArrayList;
import java.util.List;

class UserServiceTest {

    private static final List<String> users = new ArrayList<>();
}

If one test modifies the list, another test may be affected.

Prefer

import org.junit.jupiter.api.BeforeEach;

import java.util.ArrayList;
import java.util.List;

class UserServiceTest {

    private List<String> users;

    @BeforeEach
    void setUp() {
        users = new ArrayList<>();
    }
}

Each test gets a fresh list.


11. Use @BeforeEach for Fresh Setup

If several tests need the same setup, use @BeforeEach.

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

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

class ShoppingCartTest {

    private ShoppingCart cart;

    @BeforeEach
    void setUp() {
        cart = new ShoppingCart();
    }

    @Test
    void cartStartsEmpty() {
        assertEquals(0, cart.getItemCount());
    }

    @Test
    void addingItemIncreasesItemCount() {
        cart.addItem("Book");

        assertEquals(1, cart.getItemCount());
    }
}

This keeps tests independent and avoids repeated setup code.


12. Do Not Put Assertions in Setup Methods

Setup methods should prepare test data, not verify behavior.

Avoid

@BeforeEach
void setUp() {
    calculator = new Calculator();

    assertNotNull(calculator);
}

Prefer

@BeforeEach
void setUp() {
    calculator = new Calculator();
}

@Test
void calculatorIsCreated() {
    assertNotNull(calculator);
}

Assertions are clearer when they are inside test methods.


13. Be Careful with @BeforeAll

In JUnit 5, @BeforeAll usually needs to be static.

import org.junit.jupiter.api.BeforeAll;

class DatabaseTest {

    @BeforeAll
    static void connectToDatabase() {
        System.out.println("Connect to database");
    }
}

Use @BeforeAll only for setup that should happen once for the entire test class.

For normal per-test setup, prefer @BeforeEach.


14. Avoid Overusing Mocks

Mocks are useful, especially when testing services that depend on repositories, APIs, or other components. But too many mocks can make tests tightly coupled to implementation details.

A good rule:

  • Mock external dependencies.
  • Avoid mocking simple value objects.
  • Avoid mocking the class you are actually testing.
  • Prefer real objects when they are simple and fast.

For example, it usually makes sense to mock a repository:

@Mock
private UserRepository userRepository;

But it usually does not make sense to mock a simple domain object like:

User user = mock(User.class);

When this would be clearer:

User user = new User("Alice");

15. Avoid Testing Implementation Details

Test behavior, not private methods or internal steps.

Avoid Thinking Like This

Did this method call this private helper method?

Prefer Thinking Like This

Given this input, does the public method return the correct result?

Example:

@Test
void calculatesTotalPriceIncludingTax() {
    Order order = new Order(100);

    BigDecimal total = order.calculateTotal();

    assertEquals(new BigDecimal("110.00"), total);
}

The test checks the result, not how the result was calculated internally.


16. Do Not Ignore Failing Tests

A failing test is useful feedback. Avoid disabling tests just to make the build pass.

Avoid

@Disabled
@Test
void paymentIsProcessed() {
}

Use @Disabled only when you have a clear reason, and include a message:

@Disabled("Temporarily disabled until payment gateway test environment is available")
@Test
void paymentIsProcessed() {
}

Disabled tests should be reviewed and fixed later.


17. Avoid Slow Unit Tests

Unit tests should usually be fast. If a test starts a server, connects to a real database, calls an external API, or reads large files, it may be more of an integration test.

For unit tests:

  • Use small inputs.
  • Avoid real network calls.
  • Avoid depending on system time when possible.
  • Avoid real databases unless intentionally writing integration tests.

Slow tests are often skipped, and skipped tests do not protect your code.


18. Use Meaningful Test Data

Avoid unclear values such as:

User user = new User("x", 1);

Prefer values that explain the scenario:

User user = new User("Alice", 25);

Readable test data makes the test easier to understand.


19. Avoid Exact Assertions for Floating-Point Values

Floating-point calculations can produce small rounding differences.

Avoid

assertEquals(0.3, 0.1 + 0.2);

Prefer

assertEquals(0.3, 0.1 + 0.2, 0.000001);

The third argument is the allowed difference, often called the delta.


20. Keep Tests Readable

A good test should be easy to read like a small example of how the code should behave.

Good habits include:

  • Use descriptive test names.
  • Keep each test focused.
  • Use Arrange, Act, Assert.
  • Avoid unnecessary comments.
  • Avoid complex logic.
  • Use clear expected values.
  • Use helper methods only when they improve readability.

Example of a Clean JUnit Test

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

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

class BankAccountTest {

    private BankAccount account;

    @BeforeEach
    void setUp() {
        account = new BankAccount(100);
    }

    @Test
    void depositIncreasesBalance() {
        account.deposit(50);

        assertEquals(150, account.getBalance());
    }

    @Test
    void withdrawDecreasesBalance() {
        account.withdraw(30);

        assertEquals(70, account.getBalance());
    }

    @Test
    void withdrawThrowsExceptionWhenAmountExceedsBalance() {
        IllegalArgumentException exception = assertThrows(
                IllegalArgumentException.class,
                () -> account.withdraw(150)
        );

        assertEquals("Insufficient balance", exception.getMessage());
    }
}

This test class is good because:

  • Each test is independent.
  • Setup is done with @BeforeEach.
  • Test names describe behavior.
  • Assertions are specific.
  • Exception testing is done correctly.
  • Each test checks one clear behavior.

Summary

To avoid common mistakes when writing JUnit tests:

  • Keep tests independent.
  • Use clear, descriptive test names.
  • Follow Arrange, Act, Assert.
  • Use the right assertion.
  • Put expected exceptions inside assertThrows().
  • Avoid sharing mutable state.
  • Use @BeforeEach for fresh setup.
  • Avoid putting assertions in lifecycle methods.
  • Do not test implementation details.
  • Keep unit tests fast and focused.
  • Use meaningful test data.

The best JUnit tests are straightforward, reliable, and easy to understand.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.