Spring Boot Testing – A Simple and Practical Guide
Tests are what let you change code with confidence. A good test suite tells you within seconds whether your last change broke something, long before a user notices. Spring Boot comes with everything you need to write tests at every level, from tiny unit tests to tests that start the whole application against a real database.
This guide walks through each kind of test with working examples from an online bookstore: plain unit tests, mocks, controller and repository tests, full integration tests, real databases with Testcontainers, test data and coverage.
1. Why Testing Matters
- Catch bugs early, when they are cheapest to fix.
- Change code safely: refactor knowing the tests will warn you about broken behavior.
- Document behavior: a well-named test explains what the code is supposed to do.
- Release with confidence, with automated checks on every build.
Without tests, every change has to be checked by hand, which becomes slower and less reliable as the application grows.
2. The Testing Pyramid
Different kinds of tests trade speed for realism. A healthy test suite has many fast tests and fewer slow ones:
/\
/ \ Few: full integration tests (@SpringBootTest)
/----\
/ \ Some: slice tests (@WebMvcTest, @DataJpaTest)
/--------\
/ \ Many: unit tests (JUnit + Mockito, no Spring)
/____________\
| Test type | What it checks | Starts Spring? | Typical speed |
|---|---|---|---|
| Unit test | One class, with its dependencies replaced by mocks | No | Milliseconds |
| Slice test | One layer, such as controllers or repositories | Partly | About a second |
| Integration test | Several layers working together | Fully | Several seconds |
Put most of your business logic under fast unit tests, use slice tests for the web and data layers, and keep a smaller number of integration tests for the most important end-to-end paths.
3. What spring-boot-starter-test Includes
Every Spring Boot project generated by Spring Initializr already contains the test starter:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
| Library | Used for |
|---|---|
| JUnit Jupiter (JUnit 5 and later) | Writing and running tests |
| AssertJ | Readable assertions, such as assertThat(total).isEqualTo(...) |
| Mockito | Creating mock objects |
| Spring Test and Spring Boot Test | Loading Spring contexts, MockMvc and test slices |
| JsonPath and JSONassert | Checking JSON responses |
| Hamcrest | An alternative style of assertions |
One dependency, and every tool in this guide is ready to use. Spring Boot manages compatible versions of all of them.
4. Unit Testing with JUnit
A unit test checks one class on its own, without Spring. Start with plain business logic, such as this discount calculator:
public class DiscountCalculator {
public BigDecimal discountFor(BigDecimal orderTotal, boolean member) {
if (orderTotal.signum() < 0) {
throw new IllegalArgumentException("Order total cannot be negative");
}
BigDecimal rate = BigDecimal.ZERO;
if (orderTotal.compareTo(new BigDecimal("100")) >= 0) {
rate = new BigDecimal("0.10"); // 10% from 100 upwards
}
if (member) {
rate = rate.add(new BigDecimal("0.05")); // members get 5% extra
}
return orderTotal.multiply(rate).setScale(2, RoundingMode.HALF_UP);
}
}
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import java.math.BigDecimal;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
class DiscountCalculatorTest {
private final DiscountCalculator calculator = new DiscountCalculator();
@Test
@DisplayName("Orders below 100 get no discount for non-members")
void noDiscountForSmallOrders() {
assertThat(calculator.discountFor(new BigDecimal("80"), false))
.isEqualByComparingTo("0.00");
}
@ParameterizedTest(name = "total {0}, member {1} -> discount {2}")
@CsvSource({
"50, true, 2.50",
"100, false, 10.00",
"200, true, 30.00"
})
void calculatesDiscounts(BigDecimal total, boolean member, BigDecimal expected) {
assertThat(calculator.discountFor(total, member)).isEqualByComparingTo(expected);
}
@Test
void rejectsNegativeTotals() {
assertThatThrownBy(() -> calculator.discountFor(new BigDecimal("-1"), false))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("negative");
}
}
| Feature | Purpose |
|---|---|
@Test | Marks a test method |
@DisplayName | A readable name shown in reports |
@ParameterizedTest + @CsvSource | Runs the same test with several sets of data |
assertThat(...) | AssertJ’s readable, chainable assertions |
assertThatThrownBy(...) | Checks that code throws the expected exception |
Use isEqualByComparingTo for BigDecimal: 10.0 and 10.00 are numerically equal, but not equal according to equals().
5. Mocking Dependencies with Mockito
Most classes depend on others, such as repositories or remote clients. In a unit test, replace them with mocks: fake objects whose behavior you control:
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final InventoryClient inventoryClient;
private final DiscountCalculator discountCalculator;
public OrderService(OrderRepository orderRepository, InventoryClient inventoryClient,
DiscountCalculator discountCalculator) {
this.orderRepository = orderRepository;
this.inventoryClient = inventoryClient;
this.discountCalculator = discountCalculator;
}
public Order placeOrder(String isbn, int quantity, BigDecimal price, boolean member) {
if (!inventoryClient.isAvailable(isbn, quantity)) {
throw new OutOfStockException(isbn);
}
BigDecimal total = price.multiply(BigDecimal.valueOf(quantity));
BigDecimal discount = discountCalculator.discountFor(total, member);
return orderRepository.save(new Order(isbn, quantity, total.subtract(discount)));
}
}
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
@Mock
private OrderRepository orderRepository;
@Mock
private InventoryClient inventoryClient;
@Spy
private DiscountCalculator discountCalculator = new DiscountCalculator(); // real logic
@InjectMocks
private OrderService orderService;
@Test
void savesOrderWithDiscountedTotal() {
when(inventoryClient.isAvailable("9780134685991", 2)).thenReturn(true);
when(orderRepository.save(any(Order.class))).thenAnswer(call -> call.getArgument(0));
Order order = orderService.placeOrder("9780134685991", 2, new BigDecimal("60"), false);
assertThat(order.getTotal()).isEqualByComparingTo("108.00"); // 120 minus 10%
ArgumentCaptor<Order> saved = ArgumentCaptor.forClass(Order.class);
verify(orderRepository).save(saved.capture());
assertThat(saved.getValue().getQuantity()).isEqualTo(2);
}
@Test
void rejectsOrderWhenOutOfStock() {
when(inventoryClient.isAvailable("9780134685991", 5)).thenReturn(false);
assertThatThrownBy(() -> orderService.placeOrder("9780134685991", 5, new BigDecimal("60"), false))
.isInstanceOf(OutOfStockException.class);
verifyNoInteractions(orderRepository); // nothing was saved
}
}
| Mockito feature | Meaning |
|---|---|
@Mock | A fake object; every method returns a default unless you say otherwise |
@Spy | A real object whose calls you can still verify or override |
@InjectMocks | Creates the class under test and passes the mocks into its constructor |
when(...).thenReturn(...) | Defines what a mock returns |
verify(...) | Checks that a method was called |
ArgumentCaptor | Captures the argument passed to a mock, to inspect it |
No Spring context is started, so these tests run in milliseconds. That is why business logic in services deserves mostly unit tests.
6. Writing Readable Tests
A test should read like a short story. The Given–When–Then structure makes that easy:
@Test
void membersGetExtraDiscount() {
// Given: a book in stock and a member customer
when(inventoryClient.isAvailable("9780134685991", 1)).thenReturn(true);
when(orderRepository.save(any(Order.class))).thenAnswer(call -> call.getArgument(0));
// When: the member places an order
Order order = orderService.placeOrder("9780134685991", 1, new BigDecimal("200"), true);
// Then: they pay 15% less
assertThat(order.getTotal()).isEqualByComparingTo("170.00");
}
- Name tests after behavior, such as
rejectsOrderWhenOutOfStock, nottestPlaceOrder2. - Test one behavior per test, so a failure points to one clear problem.
- Keep tests independent: no test should rely on another having run first.
- Prefer clear values over clever setup; a reader should see the expected result at a glance.
7. Integration Testing with @SpringBootTest
An integration test starts the whole application context, so real beans work together, from controller to database:
@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("test")
class OrderFlowIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private OrderRepository orderRepository;
@MockitoBean
private InventoryClient inventoryClient; // the only external system: mocked
@Test
void placingAnOrderStoresItInTheDatabase() throws Exception {
when(inventoryClient.isAvailable("9780134685991", 1)).thenReturn(true);
mockMvc.perform(post("/api/orders")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"isbn": "9780134685991", "quantity": 1, "price": 45.00, "member": false}
"""))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.total").value(45.00));
assertThat(orderRepository.count()).isEqualTo(1);
}
}
Here, the controller, validation, service, transaction and repository all run for real. Only the inventory system, which lives outside this application, is replaced with @MockitoBean, available from Spring Boot 3.4; older versions use @MockBean.
To test against a real HTTP server on a random port, use @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT). For most cases, MockMvc is faster and just as useful.
8. Testing Controllers with @WebMvcTest
@WebMvcTest loads only the web layer: controllers, JSON conversion, validation and exception handlers. Services are mocked, so the test focuses on HTTP behavior:
@WebMvcTest(OrderController.class)
class OrderControllerTest {
@Autowired
private MockMvc mockMvc;
@MockitoBean
private OrderService orderService;
@Test
void returns201WithTheCreatedOrder() throws Exception {
when(orderService.placeOrder("9780134685991", 2, new BigDecimal("60.00"), false))
.thenReturn(new Order(1L, "9780134685991", 2, new BigDecimal("108.00")));
mockMvc.perform(post("/api/orders")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"isbn": "9780134685991", "quantity": 2, "price": 60.00, "member": false}
"""))
.andExpect(status().isCreated())
.andExpect(header().exists("Location"))
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.total").value(108.00));
}
@Test
void returns400ForInvalidQuantity() throws Exception {
mockMvc.perform(post("/api/orders")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"isbn": "9780134685991", "quantity": 0, "price": 60.00}
"""))
.andExpect(status().isBadRequest());
verifyNoInteractions(orderService);
}
}
| Check | MockMvc expression |
|---|---|
| Status code | status().isOk(), .isCreated(), .isNotFound() |
| JSON field | jsonPath("$.total").value(108.00) |
| Array size | jsonPath("$.items.length()").value(3) |
| Header | header().string("Location", ...) |
| Print everything (debugging) | .andDo(print()) |
For full examples with validation and error responses, see the Spring Boot REST API and Exception Handling tutorials.
9. Testing Repositories with @DataJpaTest
@DataJpaTest loads only the JPA layer: entities, repositories and an embedded database. Each test runs in a transaction that is rolled back afterwards, so tests never affect each other:
public interface OrderRepository extends JpaRepository<Order, Long> {
@Query("SELECT o FROM Order o WHERE o.total >= :minimum ORDER BY o.total DESC")
List<Order> findLargeOrders(@Param("minimum") BigDecimal minimum);
}
@DataJpaTest
class OrderRepositoryTest {
@Autowired
private TestEntityManager entityManager;
@Autowired
private OrderRepository orderRepository;
@Test
void findsLargeOrdersSortedByTotal() {
entityManager.persist(new Order("ISBN-1", 1, new BigDecimal("50.00")));
entityManager.persist(new Order("ISBN-2", 3, new BigDecimal("300.00")));
entityManager.persist(new Order("ISBN-3", 2, new BigDecimal("150.00")));
entityManager.flush();
List<Order> large = orderRepository.findLargeOrders(new BigDecimal("100"));
assertThat(large)
.extracting(Order::getIsbn)
.containsExactly("ISBN-2", "ISBN-3");
}
}
TestEntityManager inserts test data directly, so the test checks only the repository method itself. Test your own queries this way; there is no need to test methods Spring Data generates, such as findById.
10. Other Test Slices
| Annotation | Loads | Use it to test |
|---|---|---|
@WebMvcTest | Spring MVC controllers and web components | REST endpoints |
@DataJpaTest | JPA, repositories, an embedded database | Entities and queries |
@JsonTest | Jackson and JSON support | How objects are written to and read from JSON |
@RestClientTest | RestClient / RestTemplate with a mock server | Clients that call other APIs |
@JdbcTest | JDBC and an embedded database | Plain SQL code |
@WebFluxTest | Spring WebFlux controllers | Reactive endpoints |
Here is a client test with @RestClientTest, which fakes the remote server so no real network call is made:
@RestClientTest(InventoryClient.class)
class InventoryClientTest {
@Autowired
private InventoryClient inventoryClient;
@Autowired
private MockRestServiceServer server;
@Test
void reportsAvailableStock() {
server.expect(requestTo("/api/stock/9780134685991?quantity=2"))
.andRespond(withSuccess("""
{"available": true}
""", MediaType.APPLICATION_JSON));
assertThat(inventoryClient.isAvailable("9780134685991", 2)).isTrue();
}
}
In Spring Boot 4, the packages of these slice annotations changed as the test support was split into modules. Let your IDE add the imports for your version.
11. Testing with Real Databases (Testcontainers)
Embedded databases like H2 are fast, but they are not your production database: some SQL that works in H2 fails in PostgreSQL, and the other way round. Testcontainers starts a real database in Docker for your tests:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-testcontainers</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<scope>test</scope>
</dependency>
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Testcontainers
class OrderRepositoryPostgresTest {
@Container
@ServiceConnection
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16");
@Autowired
private OrderRepository orderRepository;
@Test
void savesAndReadsOrdersOnRealPostgres() {
Order saved = orderRepository.save(new Order("ISBN-1", 1, new BigDecimal("25.00")));
assertThat(orderRepository.findById(saved.getId())).isPresent();
}
}
@ServiceConnection (Spring Boot 3.1+) connects the application to the container automatically, and replace = NONE stops @DataJpaTest from swapping in an embedded database. With Flyway, your real migrations run against the container too; see Database Migration with Flyway.
12. Test Configuration
Give tests their own settings with a test profile:
spring.jpa.show-sql=false
logging.level.com.javabytechie=DEBUG
shop.inventory.base-url=http://localhost:9999
Activate it with @ActiveProfiles("test"). For a single value in one test class, use @TestPropertySource. For values only known at runtime, such as a port chosen by a container, use @DynamicPropertySource:
@SpringBootTest
@TestPropertySource(properties = "shop.discounts.enabled=false")
class CheckoutWithoutDiscountsTest { ... }
@SpringBootTest
class InventoryIntegrationTest {
static WireMockServer inventoryServer = new WireMockServer(0); // random port
@DynamicPropertySource
static void inventoryUrl(DynamicPropertyRegistry registry) {
inventoryServer.start();
registry.add("shop.inventory.base-url", inventoryServer::baseUrl);
}
}
To add or replace beans only for tests, use a nested @TestConfiguration class, for example to provide a fixed Clock:
@SpringBootTest
class ReportServiceTest {
@TestConfiguration
static class FixedClockConfig {
@Bean
@Primary
Clock testClock() {
return Clock.fixed(Instant.parse("2026-10-04T10:00:00Z"), ZoneOffset.UTC);
}
}
// ReportService now always sees 4 October 2026 as "today"
}
13. Test Data and Isolation
Tests must not depend on each other’s data. Choose one of these approaches:
| Approach | How | Notes |
|---|---|---|
| Roll back automatically | @Transactional on the test class, or @DataJpaTest | Simple; but code that runs in other threads, or REQUIRES_NEW, is not rolled back |
| Clean up before each test | repository.deleteAll() in @BeforeEach | Works with any transaction setup |
| Load data from SQL | @Sql("/test-data/orders.sql") | Good for larger, shared datasets |
@SpringBootTest
@Sql(scripts = "/test-data/orders.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
@Sql(scripts = "/test-data/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
class OrderReportTest {
@Autowired
private OrderReportService reportService;
@Test
void countsTodaysOrders() {
assertThat(reportService.ordersToday()).isEqualTo(3);
}
}
When you test transaction behavior itself, such as a rollback after an error, do not put @Transactional on the test; see the example in Transaction Management.
14. Measuring Test Coverage
Code coverage shows which lines your tests execute. JaCoCo is the common open-source tool for Java:
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.12</version>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
./mvnw test
# open target/site/jacoco/index.html in a browser
The report highlights untested lines and branches in red. Use it to find important code without tests, not to chase a number: 100% coverage with weak assertions proves little, while 75% coverage of the business logic with strong assertions is very valuable.
15. Common Testing Annotations
| Annotation | Purpose |
|---|---|
@Test | Marks a test method (JUnit) |
@BeforeEach / @AfterEach | Runs code before or after every test |
@ParameterizedTest | Runs one test with several inputs |
@ExtendWith(MockitoExtension.class) | Enables @Mock and @InjectMocks without Spring |
@SpringBootTest | Loads the full application context |
@WebMvcTest | Loads only the web layer |
@DataJpaTest | Loads only the JPA layer, with rollback |
@AutoConfigureMockMvc | Adds MockMvc to a @SpringBootTest |
@MockitoBean | Replaces a Spring bean with a Mockito mock (Spring Boot 3.4+; was @MockBean) |
@MockitoSpyBean | Wraps a real Spring bean in a Mockito spy (was @SpyBean) |
@ActiveProfiles | Activates profiles for the test |
@TestConfiguration | Adds beans only for tests |
@Sql | Runs SQL scripts before or after tests |
16. Best Practices
- Test behavior, not implementation: check results and side effects, not which private method was called.
- Follow the pyramid: many fast unit tests, fewer slice tests, few full integration tests.
- Use the smallest context that works: plain JUnit before
@WebMvcTest, and@WebMvcTestbefore@SpringBootTest. - Test failure paths, such as invalid input and exceptions, not only the happy path.
- Mock only what you do not own or what is slow, such as external services; use real objects for simple classes.
- Use the production database type for repository and integration tests, with Testcontainers.
- Keep tests fast and independent, so they are run often.
- Run all tests on every build, for example in your CI pipeline.
17. Common Mistakes to Avoid
| Mistake | What goes wrong | Fix |
|---|---|---|
Using @SpringBootTest for everything | Slow test suites that developers stop running | Use unit tests and slices where possible |
| Tests without assertions | They pass even when the code is wrong | Assert on results and important side effects |
| Mocking everything | Tests pass while the real objects would fail together | Mock boundaries; use real objects inside them |
| Tests depending on order or shared data | Random failures | Make each test set up its own data |
Using Thread.sleep to wait | Slow and flaky tests | Wait for a condition, for example with Awaitility |
Comparing BigDecimal with isEqualTo | 10.0 and 10.00 are reported as different | Use isEqualByComparingTo |
Still using @MockBean | Deprecation warnings; removed in Spring Boot 4 | Switch to @MockitoBean |
18. Summary
Spring Boot gives you a complete testing toolbox in one starter. Write fast unit tests with JUnit and Mockito for business logic, use @WebMvcTest and @DataJpaTest to test the web and data layers in isolation, and keep a smaller set of @SpringBootTest integration tests for complete flows. Test against your real database with Testcontainers, give tests their own configuration and data, and use coverage reports to find gaps, not to chase numbers. A test suite built this way runs quickly, catches real bugs and lets you change your code with confidence.
Frequently Asked Questions (FAQ)
Q1. Should I write unit tests or integration tests first?
Start with unit tests for business logic: they are quick to write, run in milliseconds and pinpoint problems exactly. Then add a few integration tests for the most important user journeys, such as “place an order”, to prove the pieces work together.
If time is short and the code is mostly simple data handling with little logic, a handful of well-chosen integration tests may give more value than many tiny unit tests. Let the complexity of the code guide you.
Q2. What should I not bother testing?
- Plain getters, setters and records with no logic.
- Framework code, such as Spring Data’s generated
findByIdor Jackson’s ability to serialize a record. - Configuration that only wires beans together; an integration test that starts the context already covers it.
- Third-party libraries; trust their own tests, and test how your code uses them instead.
Spend the time on code that makes decisions: calculations, rules, conditions and error handling.
Q3. How much test coverage is enough?
There is no magic number. Many teams aim for roughly 70–80% line coverage overall, with higher coverage for core business logic and lower for simple glue code.
More important than the number:
- Are the risky parts, such as payments, pricing and permissions, thoroughly tested?
- Do tests assert meaningful results, or just execute code?
- Are failure paths tested, not only the happy path?
Use coverage to find untested important code, and avoid making it a target that people game with empty tests.
Q4. What is the difference between a mock, a stub and a spy?
| Type | Behavior | Mockito |
|---|---|---|
| Stub | Returns fixed answers so the test can run | when(repo.findById(1L)).thenReturn(...) |
| Mock | A fake you also verify interactions on | verify(repo).save(any()) |
| Spy | A real object, with some methods optionally overridden and calls verifiable | @Spy, @MockitoSpyBean |
In Mockito, the same @Mock object can act as a stub and a mock. Use spies sparingly: needing to override parts of a real object often means the class is doing too much.
Q5. Why does Mockito fail with UnnecessaryStubbingException?
With MockitoExtension, Mockito runs in strict mode: if you stub a method with when(...) but the test never calls it, the test fails. This catches leftover or wrong stubs, for example a stub with the wrong argument, which would otherwise hide bugs.
Fix it by removing the unused stub, or by correcting its arguments. Only if a stub is genuinely shared and unused in some tests, mark it as lenient:
lenient().when(clock.instant()).thenReturn(Instant.parse("2026-10-04T10:00:00Z"));
Q6. How do I test a private method?
You don’t, at least not directly. Private methods are details of how a class works. Test them through the public methods that use them, by choosing inputs that exercise each path.
If a private method is so complex that you badly want to test it on its own, that is a signal: move it into its own small class with a public method, and test that class directly. The design usually becomes clearer as well.
Q7. How do I test code that uses the current date or time?
Calls like LocalDate.now() make tests unpredictable: a test may pass today and fail at midnight or on the last day of the month. Inject a Clock instead:
@Service
public class LoanService {
private final Clock clock;
public LoanService(Clock clock) {
this.clock = clock;
}
public boolean isOverdue(Loan loan) {
return LocalDate.now(clock).isAfter(loan.getDueDate());
}
}
// In the test
Clock fixed = Clock.fixed(Instant.parse("2026-10-04T10:00:00Z"), ZoneOffset.UTC);
LoanService service = new LoanService(fixed);
In production, define a bean returning Clock.systemDefaultZone(). Tests can then pick any moment they need, such as a leap day.
Q8. Can I mock static methods?
Yes, current Mockito versions support it:
try (MockedStatic<UUID> uuid = mockStatic(UUID.class)) {
uuid.when(UUID::randomUUID).thenReturn(UUID.fromString("00000000-0000-0000-0000-000000000001"));
// code under test now gets this fixed UUID
}
But treat it as a last resort, for code you cannot change. In your own code, wrap the static call in a small injectable class, such as an IdGenerator bean, which is easier to replace in tests and keeps the dependency visible.
Q9. How do I test code that calls an external HTTP API?
Use a fake HTTP server, so tests never depend on a real remote system:
@RestClientTestwithMockRestServiceServerfor focused client tests, as in section 10.- WireMock for integration tests: it runs a real HTTP server on a local port and returns responses you define, including slow responses and errors.
inventoryServer.stubFor(get(urlPathEqualTo("/api/stock/9780134685991"))
.willReturn(aResponse()
.withStatus(503)
.withFixedDelay(2000))); // simulate a slow, failing service
Simulating failures is especially valuable: it is the only practical way to test timeouts, retries and circuit breakers.
Q10. Why do some of my tests fail randomly?
These “flaky” tests usually have one of these causes:
- Shared data: one test leaves records that another does not expect.
- Time: code uses the real clock, or tests wait with fixed sleeps.
- Order: tests rely on running in a particular sequence.
- Asynchronous code: the test checks a result before background work has finished.
- Unordered results: asserting a list order from a query without
ORDER BY.
Fix the cause rather than adding retries; a flaky test that is ignored is worse than no test, because people learn to distrust red builds.
Q11. Can JUnit run my tests in parallel?
Yes. Enable it in src/test/resources/junit-platform.properties:
junit.jupiter.execution.parallel.enabled=true
junit.jupiter.execution.parallel.mode.default=concurrent
This works best for unit tests, which share nothing. Tests that use a database or a shared Spring context can interfere with each other when run at the same time; keep those sequential, for example by marking their classes with @Execution(ExecutionMode.SAME_THREAD).
Q12. Is test-driven development (TDD) worth trying?
TDD means writing a failing test first, then just enough code to make it pass, then improving the code. Its main benefits:
- Every line of code has a reason to exist, proven by a test.
- Code naturally becomes easier to test, with smaller classes and clear inputs.
- You think about behavior and edge cases before implementation details.
It works especially well for business logic with clear rules, such as the discount calculator in this guide. Try it on one small feature; even if you do not use it everywhere, it improves how you design code.
Q13. Why are my main application.properties settings ignored in tests?
Because you probably have a file with the same name in src/test/resources. Both end up on the test classpath, and the test version hides the main one completely, not just the values it repeats.
Use a differently named file instead, and activate it with a profile:
src/main/resources/application.properties <- always loaded
src/test/resources/application-test.properties <- loaded with @ActiveProfiles("test"), overrides only what it sets
Now tests get all the normal settings, plus the few test-specific changes.
Q14. How do I run just one test class or one test method?
In the IDE, click the run icon next to the class or method. From the command line:
# One class
./mvnw test -Dtest=OrderServiceTest
# One method
./mvnw test -Dtest=OrderServiceTest#rejectsOrderWhenOutOfStock
# Gradle
./gradlew test --tests "*OrderServiceTest"
Running a single test is great while fixing it; run the whole suite before pushing your changes.
Q15. What is the difference between @Mock and @MockitoBean?
@Mock | @MockitoBean | |
|---|---|---|
| Comes from | Mockito | Spring Framework (6.2+) |
| Needs a Spring context | No | Yes |
| What it does | Creates a mock for you to pass in yourself, or with @InjectMocks | Replaces a bean inside the Spring context with a mock |
| Speed | Very fast | Needs the context to start |
Use @Mock in plain unit tests, and @MockitoBean in Spring tests such as @WebMvcTest, where other beans need the mock injected.
Q16. How do I test endpoints protected by Spring Security?
Add the spring-security-test dependency, then simulate a logged-in user:
@Test
@WithMockUser(username = "asha", roles = "ADMIN")
void adminCanDeleteOrders() throws Exception {
mockMvc.perform(delete("/api/orders/1").with(csrf()))
.andExpect(status().isNoContent());
}
@Test
void anonymousUsersAreRejected() throws Exception {
mockMvc.perform(delete("/api/orders/1"))
.andExpect(status().isUnauthorized());
}
Test both sides: that allowed users get through, and that everyone else is blocked. A missing security rule is a bug that only a test like the second one will catch.
Q17. How can I check what my code logs?
Spring Boot’s OutputCaptureExtension captures everything written to the console during a test:
@ExtendWith(OutputCaptureExtension.class)
class PaymentServiceTest {
@Test
void logsAWarningForLargePayments(CapturedOutput output) {
paymentService.pay(new BigDecimal("50000"));
assertThat(output).contains("Large payment");
}
}
Use it for logs that matter, such as audit or security warnings. Most log lines are not worth asserting on, because their wording changes often.
Q18. Should I use AssertJ or JUnit’s own assertions?
Both work, but AssertJ, which is included in the test starter, is usually easier to read and gives better failure messages:
// JUnit
assertEquals(2, orders.size());
assertTrue(orders.get(0).getTotal().compareTo(BigDecimal.TEN) > 0);
// AssertJ: reads like a sentence
assertThat(orders).hasSize(2);
assertThat(orders.get(0).getTotal()).isGreaterThan(BigDecimal.TEN);
AssertJ also has rich support for collections, exceptions and dates. Pick one style for the project and use it consistently.
Q19. How do I keep test data setup short and readable?
When many tests need similar objects, a small test data builder keeps each test focused on what is special about it:
public class OrderTestData {
private String isbn = "9780134685991";
private int quantity = 1;
private BigDecimal total = new BigDecimal("45.00");
public static OrderTestData anOrder() { return new OrderTestData(); }
public OrderTestData withQuantity(int quantity) { this.quantity = quantity; return this; }
public OrderTestData withTotal(String total) { this.total = new BigDecimal(total); return this; }
public Order build() { return new Order(isbn, quantity, total); }
}
// In a test: only the relevant detail is visible
Order bigOrder = anOrder().withTotal("500.00").build();
Sensible defaults for everything, plus methods to change just what matters, make tests shorter and easier to understand.
Q20. How do I know my tests would actually catch bugs?
Coverage only shows which lines ran, not whether the tests would notice if those lines were wrong. Mutation testing answers the real question: a tool such as PIT makes small changes to your code, for example turning >= into >, and checks whether any test fails.
- A test fails: the change was “killed”, and your tests protect that code.
- All tests still pass: the change “survived”, revealing a missing or weak assertion.
For example, if no test checks an order of exactly 100, changing >= 100 to > 100 in the discount rule survives, showing that the boundary case is untested. Run mutation testing occasionally on your most important classes.