Automated tests let you change code with confidence, and JUnit is the standard testing framework in Java.
JUnit 5 (Jupiter) is the standard unit-testing framework. Tests live under src/test/java, in the same package as the code they test, and run with mvn test or ./gradlew test (Gradle needs useJUnitPlatform() in its test task).
A test is a method annotated with @Test. It passes if it completes without throwing; assertions throw when expectations fail:
assertEquals(expected, actual)— expected value first, so failure messages read correctly.assertTrue,assertFalse,assertNull,assertNotNull.assertThrows(SomeException.class, () -> code())returns the exception so you can check its message.assertAll(...)reports several failures at once instead of stopping at the first.
Test classes and methods don't need to be public in JUnit 5. @BeforeEach runs before every test (a fresh setup for each), and @AfterEach after. JUnit creates a new instance of the test class per test method, so tests don't share instance fields.
Parameterized tests run one test with many inputs: annotate with @ParameterizedTest and supply data with @ValueSource, @CsvSource or @MethodSource.
Good unit tests are fast, independent and deterministic. Structure each as arrange–act–assert, give it a name that states the behaviour (rejectsNegativeAmounts), and test one behaviour per method. Code against interfaces so you can pass simple fakes; libraries like Mockito and AssertJ (fluent assertions) are common additions.
Don't chase 100% coverage. Focus on logic with branches, edge cases (empty, null, boundaries) and bugs you have fixed, so they stay fixed.
Example
package com.example; // src/test/java/com/example/PriceCalculatorTest.java
import org.junit.jupiter.api.*;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import static org.junit.jupiter.api.Assertions.*;
class PriceCalculatorTest {
PriceCalculator calc;
@BeforeEach
void setUp() { calc = new PriceCalculator(0.20); } // fresh object per test
@Test
void addsTax() { assertEquals(120.0, calc.withTax(100.0), 1e-9); }
@Test
void rejectsNegativeAmounts() {
var e = assertThrows(IllegalArgumentException.class, () -> calc.withTax(-1));
assertTrue(e.getMessage().contains("negative"));
}
@ParameterizedTest
@CsvSource({"0, 0", "10, 12", "2.5, 3"})
void manyInputs(double net, double gross) { assertEquals(gross, calc.withTax(net), 1e-9); }
}Remember
- Annotate with
@Test; put expected before actual inassertEquals assertThrowschecks error paths;@ParameterizedTestcovers many inputs- Each test gets a fresh instance — keep tests independent and fast
Watch out for
- Comparing doubles with
assertEquals(a, b)without a delta and failing on rounding noise.