Let’s face it — many unit tests out there are bad. They’re flaky, hard to read, full of copy-paste logic, and often say everything passed while your production code silently dies.

That’s not what testing is supposed to be.

In this post, we’re diving into JUnit 5 and showing you how to write tests that are clean, useful, and don't suck. Whether you’re new to testing or need a reality check on your current approach, this is the upgrade your test suite deserves.


🧪 Why JUnit Still Reigns Supreme

JUnit has been around since the early 2000s, and with JUnit 5 (Jupiter), it’s gone from “just a testing framework” to a modern, flexible powerhouse that integrates beautifully with tools like Spring Boot, Mockito, and Testcontainers.

Why JUnit?

  • Simple and expressive syntax
  • Rich annotations
  • Clean separation of lifecycle
  • Extensions for mocking, logging, conditional execution
  • IDE & CI friendly

Let’s get to the good stuff.


🛠️ Setting Up JUnit 5 in Your Project

If you’re using Maven, add this:

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

For Gradle:

groovy
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.0'

Also, make sure your build tool uses the JUnit Platform.


🧱 The Anatomy of a Good Unit Test

Let’s look at a basic (but solid) example:

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

class CalculatorTest {

    @Test
    void add_shouldReturnCorrectSum() {
        Calculator calc = new Calculator();
        int result = calc.add(2, 3);
        assertEquals(5, result, "Expected 2 + 3 to equal 5");
    }
}
Good signs:
  • The test name describes behavior
  • No extra setup
  • Assertion is precise
  • Logic is in the class under test, not the test itself

📦 Use the Right Test Naming Convention

Avoid:

java
@Test void test1() {}
@Test void user() {}

Instead use:

  • shouldReturnFalseWhenPasswordIsEmpty()
  • add_shouldThrowWhenInputIsNull()

💡 Tools like Google Truth and AssertJ make assertions even more expressive.


🔁 Lifecycle Hooks

JUnit 5 gives you elegant control over test setup/teardown.

java
@BeforeEach
void setUp() {
    // runs before every @Test
}

@AfterEach
void tearDown() {
    // cleanup
}

@BeforeAll
static void initAll() {
    // expensive setup — run once
}

Use these to avoid repeated setup logic.


🧪 Parameterized Tests

Instead of writing 5 nearly identical tests…

java
@ParameterizedTest
@CsvSource({
    "2, 3, 5",
    "10, 5, 15",
    "-1, 1, 0"
})
void add_shouldWorkWithVariousInputs(int a, int b, int expected) {
    Calculator calc = new Calculator();
    assertEquals(expected, calc.add(a, b));
}

You get cleaner code, fewer copy-paste bugs, and easier coverage.


🧨 Writing Tests That Catch Bugs

Here's what bad tests usually look like:

java

@Test
void addTest() {
    assertTrue(new Calculator().add(2, 3) > 0);
}

This passes even if add() returns 6. It's vague.

🔁 Fix it with:

java

assertEquals(5, new Calculator().add(2, 3));

Also: one assertion per behavior unless they’re tightly coupled.


🧰 Useful Assertions

JUnit 5 has assertAll, assertThrows, and assertTimeout — often overlooked but super useful.

java

assertAll("grouped assertions",
    () -> assertEquals(5, calc.add(2, 3)),
    () -> assertTrue(calc.isPositive(5))
);
java
CopyEdit
assertThrows(IllegalArgumentException.class, () -> calc.divide(1, 0));
java
CopyEdit
assertTimeout(Duration.ofMillis(100), () -> longRunningMethod());

Use them to write meaningful tests with better feedback.


🧙 Use Mocks When Needed (But Don’t Overuse Them)

Mocks let you isolate your tests. With Mockito:

java

@Mock
UserRepository userRepo;

@InjectMocks
UserService userService;

@BeforeEach
void setUp() {
    MockitoAnnotations.openMocks(this);
}

@Test
void shouldReturnUserById() {
    when(userRepo.findById(1L)).thenReturn(Optional.of(new User("Jane")));
    User user = userService.getUserById(1L);
    assertEquals("Jane", user.getName());
}

✅ Use mocks to isolate dependencies.

❌ Don't mock everything — real integration tests matter too.

🌐 Integration Testing with Spring Boot

Use @SpringBootTest to spin up the Spring context:

java

@SpringBootTest
class UserServiceIntegrationTest {

    @Autowired
    private UserService userService;

    @Test
    void shouldReturnUser() {
        User user = userService.getUserById(1L);
        assertNotNull(user);
    }
}

Use @DataJpaTest, @WebMvcTest, and @MockBean to scope it down.


🧼 Keep Your Test Code Clean

Treat test code like production code.

Best practices:

  • Avoid duplicate setup logic → use utility methods or test builders
  • Avoid magic numbers → use constants
  • Don’t test private methods → test via public interfaces
  • Avoid unnecessary mocks → real code = real bugs found
  • Keep test classes small and modular

💣 Common Anti-Patterns (Tests That Suck)

Bad PracticeWhy It SucksToo many assertions in one testHard to pinpoint failureRandom sleeps (Thread.sleep)Makes tests flaky and slowMocking everythingNo real confidence in the logicIgnoring exceptionsHidden bugs and missed behaviorUsing generic names (test1)Makes debugging painful in CI logs


📈 Measuring Test Quality

Use tools like:

  • JaCoCo for code coverage
  • Mutation testing with PIT to check if your tests catch changes
  • Testcontainers for real-world environment simulation
Remember: 100% coverage doesn’t mean 100% confidence.
Aim for high-quality coverage over raw numbers.

🔄 How to Maintain Long-Term Test Health

  • Automate your test runs (CI/CD)
  • Run critical tests on every commit (smoke tests)
  • Mark long-running ones with @Tag("slow")
  • Use @Disabled only temporarily (track them!)

👨‍💼 For Teams & Hiring Managers

Want to assess if someone writes great tests? Don’t just ask if they “know JUnit.” Instead, check if they:

  • Write expressive test names
  • Use parameterized testing
  • Balance unit and integration tests
  • Understand mocking responsibly
  • Catch regressions fast in CI

That’s the difference between test writers and test architects — and why many businesses now hire Java developers with real-world JUnit experience.


🏁 Final Thoughts

Tests aren’t just a checkbox. They’re your safety net, documentation, and debugging ally — if you write them well.

And with JUnit 5, there’s no excuse not to write tests that are powerful, readable, and fast. Start small, refactor regularly, and treat your test suite like the critical system it is.

Because clean tests = confident shipping. 🚀