How do I configure Maven Surefire for JUnit tests?

To configure Maven Surefire for running JUnit tests, add the Maven Surefire Plugin to the <build> section of your pom.xml.

For modern JUnit 5 tests, you typically need:

  1. A JUnit 5 dependency
  2. The Maven Surefire Plugin
  3. Test classes named using Maven’s default test naming patterns

1. Add JUnit 5 Dependency

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

2. Configure Maven Surefire Plugin

Add this inside your pom.xml:

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>3.5.3</version>
        </plugin>
    </plugins>
</build>

A complete minimal Maven setup looks like this:

<project>
    <!-- project coordinates omitted -->

    <properties>
        <maven.compiler.release>25</maven.compiler.release>
        <junit.jupiter.version>5.13.4</junit.jupiter.version>
        <maven.surefire.version>3.5.3</maven.surefire.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>${junit.jupiter.version}</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>${maven.surefire.version}</version>
            </plugin>
        </plugins>
    </build>
</project>

3. Use Standard Test Class Names

By default, Surefire runs tests whose names match patterns like:

**/Test*.java
**/*Test.java
**/*Tests.java
**/*TestCase.java

For example:

import org.junit.jupiter.api.Test;

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

class CalculatorTest {

    @Test
    void shouldAddTwoNumbers() {
        int result = 2 + 3;

        assertEquals(5, result);
    }
}

4. Run the Tests

From the project directory, run:

mvn test

Maven will compile your test code and run tests using Surefire.

5. Optional: Include or Exclude Specific Tests

You can customize which tests Surefire runs:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>3.5.3</version>
    <configuration>
        <includes>
            <include>**/*Test.java</include>
            <include>**/*Tests.java</include>
        </includes>
        <excludes>
            <exclude>**/*IntegrationTest.java</exclude>
        </excludes>
    </configuration>
</plugin>

6. Optional: Run Tests by Tag

If you use JUnit 5 tags:

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

class PaymentServiceTest {

    @Test
    @Tag("fast")
    void shouldProcessPayment() {
        // test code
    }
}

Configure Surefire like this:

<configuration>
    <groups>fast</groups>
</configuration>

Or run from the command line:

mvn test -Dgroups=fast

Summary

For JUnit 5, the essential Maven Surefire configuration is:

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>5.13.4</version>
    <scope>test</scope>
</dependency>
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>3.5.3</version>
</plugin>

Then run:

mvn test

How do I understand JUnit Platform, Jupiter, and Vintage?

If you are learning modern Java testing, the words JUnit Platform, JUnit Jupiter, and JUnit Vintage can be confusing at first.

They sound like three different testing frameworks, but they are really three parts of the JUnit 5 ecosystem.

The short version is:

JUnit Platform runs tests.

JUnit Jupiter is the modern JUnit 5 programming model.

JUnit Vintage lets old JUnit 3 and JUnit 4 tests run on the JUnit 5 Platform.

Let’s break that down clearly.


The Big Picture

JUnit 5 is not one single library in the same way JUnit 4 was. It is an ecosystem made of several modules.

The three most important names are:

Name Purpose
JUnit Platform Foundation for discovering and running tests
JUnit Jupiter API and engine for writing and running JUnit 5 tests
JUnit Vintage Engine for running legacy JUnit 3 and JUnit 4 tests

A useful mental model is:

JUnit 5
├── JUnit Platform
│   ├── Test discovery
│   ├── Test execution
│   └── Integration with IDEs, Maven, Gradle, etc.
│
├── JUnit Jupiter
│   ├── @Test
│   ├── @BeforeEach
│   ├── @AfterEach
│   ├── @ParameterizedTest
│   └── Modern JUnit 5 test engine
│
└── JUnit Vintage
    └── Runs old JUnit 3 / JUnit 4 tests

1. What Is the JUnit Platform?

The JUnit Platform is the foundation of JUnit 5.

It is responsible for:

  • discovering tests
  • launching tests
  • reporting results
  • providing an API for tools such as:
    • IntelliJ IDEA
    • Eclipse
    • Maven Surefire
    • Gradle
    • build servers
    • custom test runners

The Platform itself does not define the main annotations you usually write in your test classes.

For example, this annotation:

@Test

it usually comes from JUnit Jupiter, not from the Platform.

The Platform answers the question:

How do tools find and run tests?

It does not primarily answer:

How do I write a test method?

That job belongs to Jupiter.


JUnit Platform as a Test Launcher

When you click Run Test in your IDE, the IDE does not need to understand every testing framework in detail.

Instead, it can use the JUnit Platform to discover and execute tests.

The Platform can run tests through different test engines.

A test engine is an implementation that knows how to discover and execute a specific kind of test.

For example:

Test Engine Runs
Jupiter Engine JUnit 5/Jupiter tests
Vintage Engine JUnit 3 and JUnit 4 tests
Other engines Other testing frameworks that integrate with the Platform

So the Platform is like a central test-running infrastructure.


2. What Is JUnit Jupiter?

JUnit Jupiter is what most people mean when they say “JUnit 5 tests.”

It includes two main things:

  1. JUnit Jupiter API
  2. JUnit Jupiter Engine

The API gives you annotations and assertions for writing tests.

The engine allows those tests to be discovered and executed by the JUnit Platform.


JUnit Jupiter API

When you write a modern JUnit 5 test, you usually import from:

org.junit.jupiter.api

Example:

import org.junit.jupiter.api.Test;

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

class CalculatorTest {

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

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

        assertEquals(5, result);
    }
}

This is a JUnit Jupiter test.

The annotation is:

org.junit.jupiter.api.Test

not:

org.junit.Test

That distinction matters.


Common Jupiter Annotations

JUnit Jupiter provides the modern test annotations:

Annotation Purpose
@Test Marks a test method
@BeforeEach Runs before each test
@AfterEach Runs after each test
@BeforeAll Runs once before all tests
@AfterAll Runs once after all tests
@DisplayName Gives a readable test name
@Nested Creates nested test classes
@Disabled Disables a test
@ParameterizedTest Runs the same test with different inputs

Example:

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

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

class StringFormatterTest {

    private StringFormatter formatter;

    @BeforeEach
    void setUp() {
        formatter = new StringFormatter();
    }

    @Test
    @DisplayName("Converts text to uppercase")
    void convertsTextToUppercase() {
        assertEquals("HELLO", formatter.uppercase("hello"));
    }
}

JUnit Jupiter Engine

Writing the test is only half of the story.

To run Jupiter tests, the JUnit Platform needs the Jupiter Engine.

In a Maven project, you commonly add:

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

The junit-jupiter dependency is a convenient aggregate dependency that brings in the Jupiter API and engine.

With Gradle:

dependencies {
    testImplementation 'org.junit.jupiter:junit-jupiter:5.13.4'
}

test {
    useJUnitPlatform()
}

In modern Gradle Kotlin DSL:

dependencies {
    testImplementation("org.junit.jupiter:junit-jupiter:5.13.4")
}

tasks.test {
    useJUnitPlatform()
}

The important idea is:

Jupiter gives you the annotations and behavior for modern JUnit 5 tests, while the Platform provides the infrastructure to run them.


3. What Is JUnit Vintage?

JUnit Vintage exists for backward compatibility.

It lets you run old JUnit 3 and JUnit 4 tests on the JUnit Platform.

This is useful when a project is migrating from JUnit 4 to JUnit 5.

Suppose you have an old JUnit 4 test:

import org.junit.Test;

import static org.junit.Assert.assertEquals;

public class LegacyCalculatorTest {

    @Test
    public void addsTwoNumbers() {
        Calculator calculator = new Calculator();

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

        assertEquals(5, result);
    }
}

Notice the imports:

import org.junit.Test;
import static org.junit.Assert.assertEquals;

That is JUnit 4 style.

A Jupiter test would use:

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

If your project includes the Vintage Engine, the JUnit Platform can discover and run that old JUnit 4 test.


Vintage Is for Running Old Tests, Not Writing New Ones

You generally should not write new tests using Vintage.

For new tests, prefer Jupiter.

Use Vintage only when:

  • you already have JUnit 3 or JUnit 4 tests
  • you want to migrate gradually
  • you need old tests to keep running during the migration

In Maven, Vintage usually looks like this:

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

In Gradle:

dependencies {
    testImplementation 'org.junit.vintage:junit-vintage-engine:5.13.4'
}

test {
    useJUnitPlatform()
}

Platform vs. Jupiter vs. Vintage

Here is the clearest comparison:

Component What it is You use it for
JUnit Platform Test launching infrastructure Running tests through IDEs/build tools
JUnit Jupiter Modern JUnit 5 programming and extension model Writing new JUnit 5 tests
JUnit Vintage Legacy test engine Running old JUnit 3/JUnit 4 tests

Another way to say it:

JUnit Platform = test runtime infrastructure
JUnit Jupiter  = modern JUnit 5 tests
JUnit Vintage  = compatibility layer for old tests

Why Did JUnit 5 Split Things This Way?

JUnit 4 was more monolithic. JUnit 5 was redesigned to be more modular.

This design allows the JUnit Platform to run different kinds of tests through different engines.

For example, the Platform can support:

  • JUnit Jupiter tests
  • JUnit Vintage tests
  • third-party test engines
  • custom test frameworks

This makes JUnit 5 more flexible than older versions.

The key design idea is:

The Platform does not care what kind of test you wrote, as long as there is a test engine that knows how to run it.


A Practical Example

Imagine your project has both new and old tests.

src/test/java
├── CalculatorJupiterTest.java
└── LegacyCalculatorJUnit4Test.java

The new test:

import org.junit.jupiter.api.Test;

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

class CalculatorJupiterTest {

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

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

The old test:

import org.junit.Test;

import static org.junit.Assert.assertEquals;

public class LegacyCalculatorJUnit4Test {

    @Test
    public void addsTwoNumbers() {
        Calculator calculator = new Calculator();

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

To run both on the JUnit Platform, you need:

  • JUnit Jupiter Engine for the Jupiter test
  • JUnit Vintage Engine for the JUnit 4 test

Maven example:

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

    <dependency>
        <groupId>org.junit.vintage</groupId>
        <artifactId>junit-vintage-engine</artifactId>
        <version>5.13.4</version>
        <scope>test</scope>
    </dependency>
</dependencies>

Now the JUnit Platform can run both types of tests.


Common Import Mistake

One of the most common mistakes during migration is mixing JUnit 4 and JUnit Jupiter imports.

For JUnit Jupiter, use:

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

For JUnit 4, use:

import org.junit.Test;
import static org.junit.Assert.assertEquals;

Do not accidentally write a Jupiter-style test with JUnit 4 imports.

For example, this is suspicious:

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

public class MixedTest {

    @BeforeEach
    void setUp() {
    }

    @Test
    public void testSomething() {
    }
}

Here, @BeforeEach is from Jupiter, but @Test is from JUnit 4.

That can lead to confusing behavior.

A clean Jupiter version would be:

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

class CleanJupiterTest {

    @BeforeEach
    void setUp() {
    }

    @Test
    void testSomething() {
    }
}

Do I Always Need All Three?

No.

For a new Java project, you usually need:

JUnit Platform + JUnit Jupiter

You usually do not need Vintage.

For a project with old JUnit 4 tests, you may need:

JUnit Platform + JUnit Jupiter + JUnit Vintage

For a project that only uses JUnit 4 and has not moved to JUnit 5 infrastructure, you may not be using the JUnit Platform at all.


What Should I Use Today?

For new tests, use JUnit Jupiter.

A typical modern test should look like this:

import org.junit.jupiter.api.Test;

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

class UserServiceTest {

    @Test
    void createsActiveUser() {
        UserService userService = new UserService();

        User user = userService.createUser("[email protected]");

        assertTrue(user.isActive());
    }
}

Recommended approach:

Situation Recommendation
Starting a new project Use JUnit Jupiter
Maintaining JUnit 4 tests Add Vintage temporarily
Migrating to JUnit 5 Run old tests with Vintage, write new tests with Jupiter
Finished migration Remove Vintage

Simple Analogy

Think of a theater.

JUnit Part Theater Analogy
JUnit Platform The theater building, stage, lights, and ticket system
JUnit Jupiter A modern play performed on the stage
JUnit Vintage An old classic play adapted to run on the same stage

The Platform provides the place and infrastructure.

Jupiter and Vintage provide different kinds of performances.


Final Summary

JUnit 5 is made of multiple parts:

  • JUnit Platform is the foundation that discovers and runs tests.
  • JUnit Jupiter is the modern JUnit 5 API and engine used for writing new tests.
  • JUnit Vintage is the compatibility engine for running JUnit 3 and JUnit 4 tests.

If you are writing new Java tests today, focus on JUnit Jupiter.

If your project still has older JUnit 4 tests, use JUnit Vintage temporarily so those tests can continue running on the JUnit Platform while you migrate.

How do I test Collections, Lists, Maps, and Sets in JUnit?

Testing Collections in JUnit

In JUnit, you usually test collections with assertions such as:

  • assertEquals
  • assertTrue
  • assertFalse
  • assertIterableEquals
  • assertArrayEquals
  • assertThrows

If you are using JUnit 5, import assertions from:

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

1. Testing a List

Use assertEquals when order matters.

import org.junit.jupiter.api.Test;

import java.util.List;

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

class ListTest {

    @Test
    void shouldContainExpectedItemsInOrder() {
        List<String> names = List.of("Alice", "Bob", "Charlie");

        assertEquals(3, names.size());
        assertEquals("Alice", names.get(0));
        assertEquals(List.of("Alice", "Bob", "Charlie"), names);
        assertTrue(names.contains("Bob"));
    }
}

List.equals() checks:

  1. Same size
  2. Same elements
  3. Same order

So this works well:

assertEquals(List.of("Alice", "Bob"), actualList);

2. Testing a Set

A Set does not guarantee order, so compare it with another set.

import org.junit.jupiter.api.Test;

import java.util.Set;

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

class SetTest {

    @Test
    void shouldContainExpectedUniqueItems() {
        Set<String> roles = Set.of("ADMIN", "USER");

        assertEquals(2, roles.size());
        assertTrue(roles.contains("ADMIN"));
        assertEquals(Set.of("USER", "ADMIN"), roles);
    }
}

Set.equals() ignores order, so this passes:

assertEquals(Set.of("ADMIN", "USER"), actualSet);

3. Testing a Map

Use assertEquals to compare maps by key/value pairs.

import org.junit.jupiter.api.Test;

import java.util.Map;

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

class MapTest {

    @Test
    void shouldContainExpectedEntries() {
        Map<String, Integer> scores = Map.of(
                "Alice", 95,
                "Bob", 88
        );

        assertEquals(2, scores.size());
        assertEquals(95, scores.get("Alice"));
        assertTrue(scores.containsKey("Bob"));
        assertEquals(Map.of("Bob", 88, "Alice", 95), scores);
    }
}

Map.equals() checks that both maps contain the same mappings, regardless of entry order.

assertEquals(Map.of("Alice", 95, "Bob", 88), actualMap);

4. Testing Collection Size

import org.junit.jupiter.api.Test;

import java.util.List;

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

class CollectionSizeTest {

    @Test
    void shouldHaveExpectedSize() {
        List<String> items = List.of("A", "B", "C");

        assertEquals(3, items.size());
    }
}

5. Testing Empty Collections

import org.junit.jupiter.api.Test;

import java.util.Collections;
import java.util.List;

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

class EmptyCollectionTest {

    @Test
    void shouldBeEmpty() {
        List<String> items = Collections.emptyList();

        assertTrue(items.isEmpty());
    }
}

You can also use:

assertEquals(0, items.size());

But this is usually more readable:

assertTrue(items.isEmpty());

6. Testing That a Collection Contains an Item

import org.junit.jupiter.api.Test;

import java.util.List;

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

class ContainsTest {

    @Test
    void shouldContainExpectedItem() {
        List<String> names = List.of("Alice", "Bob");

        assertTrue(names.contains("Alice"));
    }
}

For negative checks:

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

// ...

assertFalse(names.contains("Charlie"));

7. Testing List Order Explicitly

You can use assertIterableEquals for lists and other iterable collections.

import org.junit.jupiter.api.Test;

import java.util.List;

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

class IterableTest {

    @Test
    void shouldMatchExpectedOrder() {
        List<String> actual = List.of("A", "B", "C");

        assertIterableEquals(List.of("A", "B", "C"), actual);
    }
}

This checks both contents and order.


8. Testing Same Contents Regardless of Order

For lists where order does not matter, convert both to sets:

import org.junit.jupiter.api.Test;

import java.util.List;
import java.util.Set;

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

class UnorderedListTest {

    @Test
    void shouldHaveSameItemsIgnoringOrder() {
        List<String> actual = List.of("B", "A", "C");

        assertEquals(Set.of("A", "B", "C"), Set.copyOf(actual));
    }
}

Be careful: converting to a set removes duplicates.

If duplicates matter but order does not, sort both lists first:

import org.junit.jupiter.api.Test;

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

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

class SortedListTest {

    @Test
    void shouldHaveSameItemsIgnoringOrderButKeepingDuplicates() {
        List<String> actual = new ArrayList<>(List.of("B", "A", "A"));
        List<String> expected = new ArrayList<>(List.of("A", "A", "B"));

        actual.sort(Comparator.naturalOrder());
        expected.sort(Comparator.naturalOrder());

        assertEquals(expected, actual);
    }
}

9. Testing a Mutable Collection

import org.junit.jupiter.api.Test;

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

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

class MutableListTest {

    @Test
    void shouldAddItemToList() {
        List<String> items = new ArrayList<>();

        items.add("Book");

        assertEquals(1, items.size());
        assertTrue(items.contains("Book"));
    }
}

10. Testing Exceptions for Immutable Collections

Collections created with List.of, Set.of, or Map.of are immutable.

import org.junit.jupiter.api.Test;

import java.util.List;

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

class ImmutableCollectionTest {

    @Test
    void shouldThrowWhenModifyingImmutableList() {
        List<String> items = List.of("A", "B");

        assertThrows(UnsupportedOperationException.class, () -> {
            items.add("C");
        });
    }
}

11. Testing with AssertJ

If your project uses AssertJ, collection assertions are often more readable.

Maven dependency:

<dependency>
    <groupId>org.assertj</groupId>
    <artifactId>assertj-core</artifactId>
    <version>3.26.3</version>
    <scope>test</scope>
</dependency>

Example:

import org.junit.jupiter.api.Test;

import java.util.List;
import java.util.Map;
import java.util.Set;

import static org.assertj.core.api.Assertions.assertThat;

class AssertJCollectionTest {

    @Test
    void shouldTestCollectionsFluently() {
        List<String> names = List.of("Alice", "Bob");

        assertThat(names)
                .hasSize(2)
                .contains("Alice")
                .containsExactly("Alice", "Bob");

        Set<String> roles = Set.of("ADMIN", "USER");

        assertThat(roles)
                .containsExactlyInAnyOrder("USER", "ADMIN");

        Map<String, Integer> scores = Map.of("Alice", 95, "Bob", 88);

        assertThat(scores)
                .hasSize(2)
                .containsEntry("Alice", 95)
                .containsKey("Bob");
    }
}

AssertJ is especially useful for:

assertThat(list).containsExactly("A", "B");
assertThat(list).containsExactlyInAnyOrder("B", "A");
assertThat(set).contains("A");
assertThat(map).containsEntry("key", "value");
assertThat(collection).isEmpty();

Quick Reference

What you want to test JUnit assertion
List equals expected order assertEquals(expectedList, actualList)
Iterable equals expected order assertIterableEquals(expected, actual)
Set contains same items assertEquals(expectedSet, actualSet)
Map contains same entries assertEquals(expectedMap, actualMap)
Collection has size assertEquals(expectedSize, collection.size())
Collection is empty assertTrue(collection.isEmpty())
Collection contains item assertTrue(collection.contains(item))
Collection does not contain item assertFalse(collection.contains(item))

Recommended Style

For standard JUnit 5 tests:

assertEquals(List.of("A", "B"), actualList);
assertEquals(Set.of("A", "B"), actualSet);
assertEquals(Map.of("A", 1, "B", 2), actualMap);
assertTrue(actualList.contains("A"));
assertTrue(actualList.isEmpty());

For more readable collection tests, use AssertJ:

assertThat(actualList).containsExactly("A", "B");
assertThat(actualList).containsExactlyInAnyOrder("B", "A");
assertThat(actualMap).containsEntry("A", 1);

How do I compare objects in JUnit tests?

In JUnit tests, you usually compare objects with assertions, not with ==.

The most common choices are:

  • assertEquals(expected, actual) — compares objects using their equals() method.
  • assertSame(expected, actual) — checks whether both references point to the same object.
  • AssertJ’s assertThat(actual).isEqualTo(expected) — a more fluent alternative.
  • Field-by-field assertions — useful when you only care about some properties.

1. Comparing Objects with assertEquals()

In JUnit 5, use:

import org.junit.jupiter.api.Test;

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

class PersonTest {

    @Test
    void shouldCompareObjectsUsingEquals() {
        Person expected = new Person("Alice", 30);
        Person actual = new Person("Alice", 30);

        assertEquals(expected, actual);
    }
}

This works only if Person correctly overrides equals().

Example:

import java.util.Objects;

public class Person {
    private final String name;
    private final int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }

        if (!(o instanceof Person person)) {
            return false;
        }

        return age == person.age &&
                Objects.equals(name, person.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
}

Now two different Person instances with the same values are considered equal.

2. Do Not Use == for Value Comparison

This checks whether both variables refer to the same object in memory:

Person p1 = new Person("Alice", 30);
Person p2 = new Person("Alice", 30);

assertEquals(p1, p2); // compares values using equals()

But this would fail:

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

assertTrue(p1 == p2); // false, because they are different objects

Use == only when you intentionally want to check reference identity.

3. Use assertSame() for Reference Comparison

If you want to verify that two references point to the exact same object, use assertSame():

import org.junit.jupiter.api.Test;

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

class PersonTest {

    @Test
    void shouldReferToSameObject() {
        Person person = new Person("Alice", 30);

        Person samePerson = person;

        assertSame(person, samePerson);
    }
}

Use assertSame() for identity checks, not regular value comparison.

4. Compare Individual Fields

Sometimes you do not need full object equality. You may only care about specific fields:

import org.junit.jupiter.api.Test;

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

class PersonTest {

    @Test
    void shouldComparePersonFields() {
        Person actual = new Person("Alice", 30);

        assertEquals("Alice", actual.getName());
        assertEquals(30, actual.getAge());
    }
}

This is useful when:

  • the class does not override equals(),
  • only some fields matter,
  • generated fields like id, timestamps, or audit fields should be ignored.

5. Use AssertJ for More Readable Object Assertions

AssertJ provides fluent assertions:

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

class PersonTest {

    @Test
    void shouldCompareObjectsWithAssertJ() {
        Person expected = new Person("Alice", 30);
        Person actual = new Person("Alice", 30);

        assertThat(actual).isEqualTo(expected);
    }
}

You can also compare specific properties:

assertThat(actual)
        .extracting(Person::getName, Person::getAge)
        .containsExactly("Alice", 30);

Or compare objects recursively:

assertThat(actual)
        .usingRecursiveComparison()
        .isEqualTo(expected);

Recursive comparison is helpful when objects contain nested objects and you do not want to rely on every class implementing equals().

6. Comparing Lists of Objects

For lists, assertEquals() also uses equals() on each element:

import org.junit.jupiter.api.Test;

import java.util.List;

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

class PersonListTest {

    @Test
    void shouldCompareListsOfPeople() {
        List<Person> expected = List.of(
                new Person("Alice", 30),
                new Person("Bob", 25)
        );

        List<Person> actual = List.of(
                new Person("Alice", 30),
                new Person("Bob", 25)
        );

        assertEquals(expected, actual);
    }
}

The order must match. If order does not matter, AssertJ is often clearer:

assertThat(actual)
        .containsExactlyInAnyOrderElementsOf(expected);

7. Common Rule of Thumb

Use this:

assertEquals(expected, actual);

when you want to compare object values.

Use this:

assertSame(expected, actual);

when you want to check that both variables refer to the exact same object.

Avoid this for object value comparison:

expected == actual

because it checks identity, not logical equality.

Summary

Goal Recommended Assertion
Compare object values assertEquals(expected, actual)
Compare object references assertSame(expected, actual)
Compare selected fields assertEquals(expectedName, actual.getName())
Compare nested objects without relying on equals() AssertJ usingRecursiveComparison()
Compare lists in order assertEquals(expectedList, actualList)
Compare lists ignoring order AssertJ containsExactlyInAnyOrderElementsOf()

In most JUnit tests, object comparison starts with assertEquals(expected, actual), as long as the class has a proper equals() implementation.

How do I use AssertJ with JUnit for better assertions?

You can use AssertJ with JUnit to write more readable, fluent assertions than standard JUnit assertions.

1. Add AssertJ Dependency

If you use Maven:

<dependency>
    <groupId>org.assertj</groupId>
    <artifactId>assertj-core</artifactId>
    <version>3.26.3</version>
    <scope>test</scope>
</dependency>

If you use Gradle:

testImplementation("org.assertj:assertj-core:3.26.3")

2. Use AssertJ in a JUnit Test

Import assertThat statically:

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

class UserServiceTest {

    @Test
    void shouldReturnUserName() {
        String name = "Alice";

        assertThat(name)
                .isNotNull()
                .startsWith("A")
                .endsWith("e")
                .hasSize(5);
    }
}

3. AssertJ vs. JUnit Assertions

JUnit:

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

assertEquals("Alice", name);
assertTrue(name.startsWith("A"));

AssertJ:

import static org.assertj.core.api.Assertions.assertThat;

assertThat(name)
        .isEqualTo("Alice")
        .startsWith("A");

AssertJ reads more naturally and usually gives better failure messages.

4. Common AssertJ Examples

Strings

assertThat(email)
        .isNotBlank()
        .contains("@")
        .endsWith(".com");

Numbers

assertThat(price)
        .isPositive()
        .isGreaterThan(10)
        .isLessThanOrEqualTo(100);

Collections

assertThat(users)
        .hasSize(3)
        .extracting(User::getName)
        .containsExactly("Alice", "Bob", "Charlie");

Objects

assertThat(user)
        .isNotNull()
        .extracting(User::getName, User::getEmail)
        .containsExactly("Alice", "[email protected]");

Exceptions

assertThatThrownBy(() -> userService.findById(-1L))
        .isInstanceOf(IllegalArgumentException.class)
        .hasMessageContaining("id");

Or with JUnit + AssertJ:

IllegalArgumentException exception =
        assertThrows(IllegalArgumentException.class, () -> userService.findById(-1L));

assertThat(exception)
        .hasMessageContaining("id");

5. Example with Spring/JUnit Test

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

class CalculatorTest {

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

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

        assertThat(result).isEqualTo(5);
    }
}

6. Helpful AssertJ Tips

Use as() to describe assertions:

assertThat(user.getEmail())
        .as("user email should be valid")
        .contains("@");

Use recursive comparison for objects:

assertThat(actualUser)
        .usingRecursiveComparison()
        .isEqualTo(expectedUser);

Use containsExactlyInAnyOrder() when order does not matter:

assertThat(roles)
        .containsExactlyInAnyOrder("ADMIN", "USER");

Recommended Pattern

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

@Test
void shouldDoSomething() {
    // arrange
    User user = new User("Alice");

    // act
    String result = user.getName();

    // assert
    assertThat(result).isEqualTo("Alice");
}

In short: add assertj-core, statically import assertThat, and replace basic JUnit assertions with fluent AssertJ assertions for clearer and more expressive tests.