Write automated tests with JUnit 5, assert expected results, run parameterized cases, and isolate code from dependencies using Mockito.
Why: JUnit is the standard framework for automated tests in Java. You mark methods with @Test; the framework runs them and reports pass or fail. Catching bugs with tests is far cheaper than catching them in production.
Add JUnit 5 to Maven (pom.xml):
Why: a test calls your code and asserts the result is what you expect. assertEquals(expected, actual) fails the test if they differ. Tests live under src/test/java, mirroring the class they cover.
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
class CalculatorTest {
@Test
void addsTwoNumbers() {
Calculator calc = new Calculator();
assertEquals(5, calc.add(2, 3)); // expected 5, got calc.add(2, 3)
}
}Why: beyond equality, you assert truth, nullness, and that errors are thrown. assertThrows checks that a piece of code raises the expected exception — the way you test failure paths.
import static org.junit.jupiter.api.Assertions.*;
assertTrue(5 > 3);
assertNull(null);
assertArrayEquals(new int[]{1, 2}, new int[]{1, 2});
assertThrows(IllegalArgumentException.class, () -> {
new Account().withdraw(-10); // should reject a negative amount
});Why: to run the same test logic over many inputs, use @ParameterizedTest with a source of values instead of copy-pasting. One method, many cases, clear reporting for each.
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import static org.junit.jupiter.api.Assertions.assertTrue;
class NumbersTest {
@ParameterizedTest
@ValueSource(ints = { 2, 4, 100 })
void isEven(int number) {
assertTrue(number % 2 == 0);
}
}Why: to test one class in isolation you replace its dependencies with fakes ("mocks") that you control. Mockito creates a stand-in, lets you script what its methods return, and verifies it was called as expected — without touching a real database or network.
import static org.mockito.Mockito.*;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
class OrderServiceTest {
@Test
void usesRepository() {
UserRepository repo = mock(UserRepository.class);
when(repo.findName(1)).thenReturn("Ada"); // script the fake
OrderService service = new OrderService(repo);
assertEquals("Ada", service.ownerOf(1));
verify(repo).findName(1); // confirm it was actually called
}
}Why: mocks prove a class talks to its dependency correctly, but not that your SQL actually works. An integration test runs the real query against a real but disposable database — an in-memory H2 — created fresh for each test. Add H2 with test scope, open an in-memory connection in @BeforeEach, and assert on what truly landed in the table.
// pom.xml — H2, an in-memory database, for tests only
// <dependency>
// <groupId>com.h2database</groupId><artifactId>h2</artifactId>
// <version>2.2.224</version><scope>test</scope>
// </dependency>
import org.junit.jupiter.api.*;
import java.sql.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
class UserDaoTest {
Connection conn;
@BeforeEach
void setUp() throws SQLException {
// A fresh in-memory database for every test.
conn = DriverManager.getConnection("jdbc:h2:mem:test");
conn.createStatement().execute(
"CREATE TABLE users (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255))");
}
@AfterEach
void tearDown() throws SQLException { conn.close(); }
@Test
void persistsAndReadsBackAUser() throws SQLException {
new UserDao(conn).create("Ada"); // real INSERT
try (ResultSet rs = conn.createStatement()
.executeQuery("SELECT name FROM users")) {
rs.next();
assertEquals("Ada", rs.getString("name")); // real SELECT
}
}
}