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.