Careeroza
The Testing Pyramid
Kinds of Tests
spring-boot-starter-test (added by Initializr) brings JUnit 5, Mockito, AssertJ, Spring Test, MockMvc and JsonPath.
| Test | What runs | Speed | Use for |
|---|---|---|---|
| Unit test | One class, dependencies mocked | milliseconds | Service business rules, mappers, validators |
@WebMvcTest | Controllers + MVC, no services/DB | ~1 s | URLs, status codes, JSON, validation errors |
@DataJpaTest | Repositories + DB | ~2 s | Queries (Hibernate & JPA material) |
@SpringBootTest | The whole application | several s | End-to-end flows |
/\ few @SpringBootTest + Testcontainers
/ \
/----\ some @WebMvcTest, @DataJpaTest
/ \
/--------\ many plain JUnit unit testsA Unit Test without Any Framework
Example
A unit test is just code that calls your class with known input and checks the result. Here the service is tested with a hand-written fake repository; JUnit and Mockito make the same thing shorter.
import java.util.*;
public class Main {
static int passed = 0, failed = 0;
static void check(String name, boolean ok) {
if (ok) passed++; else failed++;
System.out.println((ok ? "PASS " : "FAIL ") + name);
}
public static void main(String[] args) {
FakeRepo repo = new FakeRepo();
repo.save(new Product(1L, "Wireless Mouse", 5));
StockService service = new StockService(repo);
check("reserve reduces stock", service.reserve(1L, 2).stock() == 3);
check("reserve saves the product", repo.saves == 2);
try {
service.reserve(1L, 10);
check("too much stock throws", false);
} catch (IllegalStateException e) {
check("too much stock throws", e.getMessage().equals("Only 3 left"));
}
check("unknown product throws", throwsNoSuchElement(() -> service.reserve(99L, 1)));
System.out.println(passed + " passed, " + failed + " failed");
}
static boolean throwsNoSuchElement(Runnable r) {
try { r.run(); return false; } catch (NoSuchElementException e) { return true; }
}
}
record Product(Long id, String name, int stock) {}
class FakeRepo {
final Map<Long, Product> rows = new HashMap<>();
int saves = 0;
Optional<Product> findById(Long id) { return Optional.ofNullable(rows.get(id)); }
Product save(Product p) { saves++; rows.put(p.id(), p); return p; }
}
class StockService {
private final FakeRepo repo;
StockService(FakeRepo repo) { this.repo = repo; }
Product reserve(Long id, int qty) {
Product p = repo.findById(id).orElseThrow();
if (p.stock() < qty) throw new IllegalStateException("Only " + p.stock() + " left");
return repo.save(new Product(p.id(), p.name(), p.stock() - qty));
}
}Output
PASS reserve reduces stock PASS reserve saves the product PASS too much stock throws PASS unknown product throws 4 passed, 0 failed