How do I use @ValueSource in JUnit parameterized tests?

In JUnit 5, @ValueSource is used with @ParameterizedTest to run the same test multiple times with different simple literal values.

Basic example

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

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

class StringTest {

    @ParameterizedTest
    @ValueSource(strings = {"racecar", "radar", "level"})
    void palindromeWordsHaveLengthGreaterThanZero(String word) {
        assertTrue(word.length() > 0);
    }
}

This test runs 3 times, once for each value:

racecar
radar
level

Supported @ValueSource types

@ValueSource supports arrays of simple values such as:

@ValueSource(strings = {"apple", "banana"})
@ValueSource(ints = {1, 2, 3})
@ValueSource(longs = {10L, 20L})
@ValueSource(doubles = {1.5, 2.5})
@ValueSource(booleans = {true, false})
@ValueSource(chars = {'a', 'b'})
@ValueSource(classes = {String.class, Integer.class})

Example with integers:

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

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

class NumberTest {

    @ParameterizedTest
    @ValueSource(ints = {2, 4, 6, 8})
    void numbersAreEven(int number) {
        assertTrue(number % 2 == 0);
    }
}

Important limitation

@ValueSource can provide only one argument per test invocation.

So this works:

@ParameterizedTest
@ValueSource(strings = {"hello", "world"})
void testSingleArgument(String value) {
    // test logic
}

But this does not work with @ValueSource:

@ParameterizedTest
@ValueSource(strings = {"hello", "world"})
void testMultipleArguments(String input, int expectedLength) {
    // invalid for @ValueSource
}

For multiple arguments, use @CsvSource, @MethodSource, or @ArgumentsSource.

Example with empty and blank strings

@ValueSource can be combined with other parameter sources:

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EmptySource;
import org.junit.jupiter.params.provider.NullSource;
import org.junit.jupiter.params.provider.ValueSource;

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

class BlankStringTest {

    @ParameterizedTest
    @NullSource
    @EmptySource
    @ValueSource(strings = {" ", "   ", "\t", "\n"})
    void stringIsBlank(String value) {
        assertTrue(value == null || value.isBlank());
    }
}

This runs with:

null
""
" "
"   "
"\t"
"\n"

Maven dependency

Make sure you have JUnit Jupiter Params on the test classpath:

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-params</artifactId>
    <scope>test</scope>
</dependency>

If you use Spring Boot, this is usually included through:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

Summary

Use @ValueSource when your parameterized test needs one simple value per run:

@ParameterizedTest
@ValueSource(strings = {"a", "b", "c"})
void test(String value) {
    // runs once for each value
}

How do I write parameterized tests in JUnit?

In JUnit 5, parameterized tests let you run the same test multiple times with different input values.

You use:

@ParameterizedTest

instead of:

@Test

Then you provide test data using a source annotation such as @ValueSource, @CsvSource, @MethodSource, or @EnumSource.

Basic Example with @ValueSource

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

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

class NumberTest {

    @ParameterizedTest
    @ValueSource(ints = {1, 2, 5, 10})
    void shouldBePositive(int number) {
        assertTrue(number > 0);
    }
}

This runs the test 4 times:

number = 1
number = 2
number = 5
number = 10

Strings with @ValueSource

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

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

class StringTest {

    @ParameterizedTest
    @ValueSource(strings = {"hello", "junit", "test"})
    void shouldNotBeBlank(String value) {
        assertFalse(value.isBlank());
    }
}

@ValueSource supports simple values such as:

@ValueSource(strings = {"a", "b"})
@ValueSource(ints = {1, 2, 3})
@ValueSource(longs = {1L, 2L})
@ValueSource(doubles = {1.5, 2.5})
@ValueSource(booleans = {true, false})

Multiple Arguments with @CsvSource

Use @CsvSource when each test case needs more than one value.

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

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

class CalculatorTest {

    @ParameterizedTest
    @CsvSource({
            "1, 2, 3",
            "5, 7, 12",
            "10, -2, 8"
    })
    void shouldAddNumbers(int a, int b, int expected) {
        int result = a + b;

        assertEquals(expected, result);
    }
}

Each CSV row maps to the test method parameters:

a, b, expected

Use @NullSource, @EmptySource, and @NullAndEmptySource

These are useful for testing validation logic.

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.NullAndEmptySource;
import org.junit.jupiter.params.provider.ValueSource;

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

class UsernameValidatorTest {

    @ParameterizedTest
    @NullAndEmptySource
    @ValueSource(strings = {" ", "   "})
    void shouldRejectBlankUsernames(String username) {
        assertTrue(username == null || username.isBlank());
    }
}

This test runs with:

null
""
" "
"   "

You can also use them separately:

@NullSource
@EmptySource
@NullAndEmptySource

Use @MethodSource for Complex Test Data

Use @MethodSource when test data is more complex or easier to build in Java code.

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

import java.util.stream.Stream;

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

class DiscountCalculatorTest {

    @ParameterizedTest
    @MethodSource("discountCases")
    void shouldCalculateDiscount(int price, double discountRate, int expectedPrice) {
        int result = (int) (price * (1 - discountRate));

        assertEquals(expectedPrice, result);
    }

    static Stream<Arguments> discountCases() {
        return Stream.of(
                Arguments.of(100, 0.10, 90),
                Arguments.of(200, 0.25, 150),
                Arguments.of(80, 0.50, 40)
        );
    }
}

The method source is usually static, unless the test class uses a per-class test instance lifecycle.

Use @EnumSource for Enum Values

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;

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

class RoleTest {

    enum Role {
        ADMIN,
        USER,
        GUEST
    }

    @ParameterizedTest
    @EnumSource(Role.class)
    void shouldHaveValidRole(Role role) {
        assertNotNull(role);
    }
}

You can also include or exclude specific enum values:

@EnumSource(value = Role.class, names = {"ADMIN", "USER"})

or:

@EnumSource(
        value = Role.class,
        names = {"GUEST"},
        mode = EnumSource.Mode.EXCLUDE
)

Naming Each Parameterized Test Run

You can customize the displayed name for each invocation:

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

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

class CalculatorTest {

    @ParameterizedTest(name = "{0} + {1} should equal {2}")
    @CsvSource({
            "1, 2, 3",
            "5, 7, 12",
            "10, -2, 8"
    })
    void shouldAddNumbers(int a, int b, int expected) {
        assertEquals(expected, a + b);
    }
}

This can show test names like:

1 + 2 should equal 3
5 + 7 should equal 12
10 + -2 should equal 8

Common placeholders include:

{index}    the invocation index
{0}        the first argument
{1}        the second argument
{arguments} all arguments

Required Dependency

For Maven:

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>5.13.4</version>
    <scope>test</scope>
</dependency>

For Gradle:

testImplementation 'org.junit.jupiter:junit-jupiter:5.13.4'

test {
    useJUnitPlatform()
}

Quick Rule of Thumb

Use:

  • @ValueSource for one simple argument
  • @CsvSource for several simple arguments
  • @MethodSource for complex objects or generated cases
  • @EnumSource for enum values
  • @NullSource, @EmptySource, or @NullAndEmptySource for null/empty validation cases

A typical parameterized test looks like this:

@ParameterizedTest
@CsvSource({
        "2, 3, 5",
        "10, 5, 15"
})
void shouldAddNumbers(int a, int b, int expected) {
    assertEquals(expected, a + b);
}

How do I decide what should and should not be unit tested?

Short answer

Unit test behavior that you own and that can break, especially logic with branches, rules, edge cases, and important failure paths.

Do not unit test code that merely repeats the framework, language, or library behavior unless you add meaningful logic around it.


Good candidates for unit tests

1. Business rules

If the code encodes a rule, test it.

Examples:

boolean isEligibleForDiscount(Customer customer) {
    return customer.isActive()
            && customer.getOrdersCount() >= 5
            && !customer.isBlacklisted();
}

This should be unit tested because it represents application-specific behavior.


2. Conditional logic and branching

Test code with if, switch, polymorphic behavior, strategy selection, validation paths, etc.

if (paymentAmount.compareTo(balance) > 0) {
    throw new InsufficientFundsException();
}

You should test:

  • amount less than balance
  • amount equal to balance
  • amount greater than balance
  • invalid amount if applicable

3. Edge cases

Unit tests are especially valuable around boundaries.

Examples:

  • zero
  • negative numbers
  • empty collections
  • null, if allowed
  • maximum/minimum values
  • duplicate values
  • time zone/date boundary cases
  • rounding and precision cases
  • permission boundaries

4. Error handling

If your code is supposed to throw, recover, retry, fallback, or translate errors, test that.

try {
    gateway.charge(request);
} catch (GatewayTimeoutException ex) {
    throw new PaymentUnavailableException("Payment provider is unavailable", ex);
}

This is worth testing because your application behavior depends on it.


5. Transformations and calculations

Any code that maps, calculates, normalizes, sorts, filters, or aggregates data is usually worth testing.

InvoiceSummary summary = invoiceCalculator.calculate(invoice);

Especially test:

  • rounding
  • currency precision
  • missing values
  • multiple item combinations
  • tax/discount rules

6. Public behavior of a class/module

Prefer testing the public API of a unit rather than every private method.

Instead of asking:

Should I test this private method?

Ask:

Is the behavior produced by this private method observable through the public method?

If yes, test through the public method.


7. Bugs that have occurred before

When you fix a bug, add a test that fails before the fix and passes after it.

This prevents regressions and documents the expected behavior.


Usually not worth unit testing

1. Simple getters and setters

Do not usually test this:

public String getName() {
    return name;
}

public void setName(String name) {
    this.name = name;
}

Especially with Lombok-generated accessors.


2. Framework wiring

Avoid unit testing things like:

@Service
@RequiredArgsConstructor
public class UserService {
    private final UserRepository userRepository;
}

You usually do not need a unit test just to verify that Spring injects dependencies. That belongs to integration tests if needed.


3. Repository methods provided by Spring Data JPA

This usually does not need a unit test:

Optional<User> findByEmail(String email);

Spring Data already tests its method parsing behavior. If the query is custom or complex, use a repository/integration test instead.


4. Trivial delegation

This is often not valuable:

public UserDto getUser(Long id) {
    return userClient.fetchUser(id);
}

Unless there is meaningful behavior such as validation, fallback, authorization, mapping, caching, or error translation.


5. Third-party library behavior

Do not unit test that Jackson serializes JSON, that BigDecimal adds numbers correctly, or that Spring MVC maps annotations correctly.

Test your configuration or behavior if you customized it.


6. Private implementation details

Avoid tests that know too much about internals.

Bad test focus:

Method calculateStepTwo() was called exactly once.

Better test focus:

Given these inputs, the invoice total is correct.


A practical decision checklist

Ask these questions:

  1. Does this code contain business logic?
    If yes, test it.

  2. Could this break in a way users or other systems would notice?
    If yes, test it.

  3. Does it have branches, edge cases, calculations, or state changes?
    If yes, test it.

  4. Would a test give me confidence to refactor it?
    If yes, test it.

  5. Am I only testing a framework, library, getter, setter, or annotation?
    If yes, probably do not unit test it.

  6. Would the test be more complex than the code being tested?
    If yes, reconsider. Maybe test at a higher level.

  7. Is this behavior better verified with an integration or end-to-end test?
    If yes, use that instead.


Rule of thumb

Use this rough guide:

Code type Unit test?
Business rules Yes
Calculations Yes
Validation logic Yes
Mapping with logic Yes
Error handling Yes
Edge cases Yes
Bug fixes Yes
Simple getters/setters Usually no
Lombok-generated methods Usually no
Spring dependency injection Usually no
Basic repository method names Usually no
Framework annotations Usually no
Third-party library behavior No

Example from a typical Spring/Jakarta application

Probably not worth unit testing:

@Getter
@Setter
@Entity
public class User {
    @Id
    private Long id;

    private String email;
}

Worth testing:

public class UserRegistrationService {

    public User register(String email, String password) {
        if (!emailValidator.isValid(email)) {
            throw new InvalidEmailException(email);
        }

        if (password.length() < 12) {
            throw new WeakPasswordException();
        }

        return userRepository.save(new User(email, passwordEncoder.encode(password)));
    }
}

Tests should cover:

  • valid registration
  • invalid email
  • weak password
  • password is encoded
  • duplicate email, if handled
  • repository failure, if translated or recovered from

Final guideline

Aim for tests that are:

  • behavior-focused
  • fast
  • clear
  • stable
  • useful during refactoring

Do not chase 100% coverage blindly. High coverage is nice, but meaningful coverage matters more.

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.

How do I follow the Arrange-Act-Assert pattern in unit tests?

The Arrange-Act-Assert pattern is a simple way to organize unit tests so they are easy to read and understand.

A unit test usually answers three questions:

  1. Arrange: What data or objects do I need?
  2. Act: What behavior am I testing?
  3. Assert: What result do I expect?

Basic Structure

@Test
void methodName_expectedBehavior() {
    // Arrange
    // Prepare objects, input values, mocks, or test data

    // Act
    // Call the method being tested

    // Assert
    // Verify the result
}

Example with JUnit 5

import org.junit.jupiter.api.Test;

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

class CalculatorTest {

    @Test
    void addReturnsSumOfTwoNumbers() {
        // Arrange
        Calculator calculator = new Calculator();
        int firstNumber = 2;
        int secondNumber = 3;

        // Act
        int result = calculator.add(firstNumber, secondNumber);

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

In this test:

  • Arrange creates the Calculator and input values.
  • Act calls calculator.add(2, 3).
  • Assert checks that the result is 5.

1. Arrange

The Arrange section prepares everything the test needs.

This can include:

  • Creating the object being tested
  • Creating input values
  • Preparing test data
  • Configuring mocks
  • Setting expected values

Example:

// Arrange
Calculator calculator = new Calculator();
int firstNumber = 2;
int secondNumber = 3;
int expectedResult = 5;

For repeated setup, you can use @BeforeEach:

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

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

class CalculatorTest {

    private Calculator calculator;

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

    @Test
    void addReturnsSumOfTwoNumbers() {
        // Arrange
        int firstNumber = 2;
        int secondNumber = 3;

        // Act
        int result = calculator.add(firstNumber, secondNumber);

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

Here, @BeforeEach handles common arrangement before every test.


2. Act

The Act section performs the action you want to test.

Usually, this should be one clear method call:

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

Try to keep the Act section small. If a test performs many actions, it may be testing too much at once.


3. Assert

The Assert section checks the result.

Examples:

assertEquals(5, result);
assertTrue(result > 0);
assertNotNull(result);

If you need to verify several related results, you can use assertAll():

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 constructorCreatesUserWithExpectedValues() {
        // Arrange
        String name = "Alice";
        int age = 25;
        String email = "[email protected]";

        // Act
        User user = new User(name, age, email);

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

Example with a Service

import org.junit.jupiter.api.Test;

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

class DiscountServiceTest {

    @Test
    void calculateDiscountReturnsTenPercentForPremiumCustomer() {
        // Arrange
        DiscountService discountService = new DiscountService();
        Customer customer = new Customer("Alice", true);
        double orderTotal = 100.00;

        // Act
        double discount = discountService.calculateDiscount(customer, orderTotal);

        // Assert
        assertEquals(10.00, discount);
    }
}

Example with Mockito

When using mocks, the mock configuration belongs in the Arrange section.

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

class OrderServiceTest {

    @Test
    void calculateTotalReturnsPriceFromRepository() {
        // Arrange
        ProductRepository productRepository = mock(ProductRepository.class);
        OrderService orderService = new OrderService(productRepository);

        when(productRepository.findPriceById(1L)).thenReturn(25.00);

        // Act
        double total = orderService.calculateTotal(1L, 2);

        // Assert
        assertEquals(50.00, total);
    }
}

Good Test Naming Helps AAA

A good test name should describe the expected behavior:

@Test
void addReturnsSumOfTwoNumbers() {
}

Other examples:

@Test
void withdrawReducesAccountBalance() {
}

@Test
void loginFailsWhenPasswordIsInvalid() {
}

@Test
void calculateTotalAppliesDiscountForPremiumCustomer() {
}

Readable test names make the Arrange-Act-Assert flow easier to understand.


Common Mistakes

1. Mixing Act and Assert

Avoid this:

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

This is short, but the action is hidden inside the assertion.

Prefer:

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

assertEquals(5, result);

This makes the tested behavior easier to see.


2. Doing Too Much in One Test

Avoid testing several behaviors in one test:

@Test
void calculatorOperationsWork() {
    Calculator calculator = new Calculator();

    assertEquals(5, calculator.add(2, 3));
    assertEquals(1, calculator.subtract(3, 2));
    assertEquals(6, calculator.multiply(2, 3));
}

Prefer separate tests:

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

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

    // Assert
    assertEquals(5, result);
}

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

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

    // Assert
    assertEquals(1, result);
}

3. Putting Assertions in Setup Methods

Avoid placing assertions in @BeforeEach. Setup methods should prepare test state.

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

Then assert behavior inside test methods:

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

Quick Checklist

Use this checklist when writing a unit test:

  • Arrange: Did I prepare the object, inputs, expected values, and mocks?
  • Act: Did I call the method or behavior being tested?
  • Assert: Did I verify the expected result?
  • Is the test focused on one behavior?
  • Can the test run independently?
  • Is the test name descriptive?

Summary

The Arrange-Act-Assert pattern keeps tests clean and readable:

@Test
void testName() {
    // Arrange
    TestObject object = new TestObject();

    // Act
    Result result = object.doSomething();

    // Assert
    assertEquals(expectedResult, result);
}

Use Arrange to prepare, Act to execute, and Assert to verify. This structure helps you write unit tests that are easier to read, debug, and maintain.