To unit test a Spring Boot service, we give the service a fake repository instead of the real one. This fake object is called a mock, and it returns the values we tell it to return while recording every call our code makes.
We mock the repository when we test the business logic of a service, such as validation or a calculation, without a database.
The following example tests a SongService with Mockito and JUnit. The annotation @ExtendWith(MockitoExtension.class) turns on Mockito for the test class, and @Mock creates the fake repository. The annotation @InjectMocks creates the real service and passes the mock into its constructor.
@ExtendWith(MockitoExtension.class)
class SongServiceTest {
@Mock
SongRepository songRepository; // fake repository, no database
@InjectMocks
SongService songService; // real service, built with the mock
@Test
void getSongReturnsSongFromRepository() {
when(songRepository.findById(1L))
.thenReturn(Optional.of(new Song(1L, "Yesterday", "The Beatles", 125))); // 1. Stub
Song song = songService.getSong(1L); // 2. Call the service
assertThat(song.getTitle()).isEqualTo("Yesterday"); // 3. Check the result: passes
verify(songRepository).findById(1L); // 4. Check the call: passes
}
}
Notice that Spring does not start at all, so the test runs in milliseconds.
Next, we look at each annotation and at when a test needs Spring, using @SpringBootTest and @MockitoBean (the Spring Boot 4 replacement for the removed @MockBean). After that, we cover thenThrow(), verifyNoMoreInteractions(), ArgumentCaptor, BDDMockito, strict stubs errors and the Mockito Java agent on Java 21 and later.
1. What Is Mockito in a Spring Boot Test?
Mockito is a library that creates mocks. A mock is a fake version of a class or interface whose behavior our test decides, and we use it in three steps.
- We stub the mock, which means we tell it what to return, for example with when(songRepository.findById(1L)).thenReturn(…).
- We call the real code that we test, which in turn calls the mock.
- We verify the result, and then check which mock methods the code called, with verify().
Spring Boot projects get Mockito from spring-boot-starter-test, which also brings JUnit Jupiter and AssertJ. Five annotations cover almost every case.
| Annotation | Package | What it does |
|---|---|---|
| @Mock | org.mockito | Creates a mock for a field. Needs MockitoExtension. |
| @InjectMocks | org.mockito | Creates the real object and passes the @Mock fields to it. Mockito tries the biggest constructor first, then setters and fields. |
| @Captor | org.mockito | Creates an ArgumentCaptor. The captor records the arguments that our code passes to a mock. |
| @MockitoBean | org.springframework.test.context.bean.override.mockito | Replaces a bean in the Spring application context with a mock. A bean is an object that Spring creates and manages. |
| @MockitoSpyBean | org.springframework.test.context.bean.override.mockito | Wraps an existing bean in a Mockito spy. A spy runs the real methods and also records the calls. |
An @InjectMocks object is a real object, not a mock. Mockito calls the SongService constructor and passes in the SongRepository mock, so every line of SongService runs for real, and only the fields marked with @Mock are fakes.
1.1. Plain Unit Test vs @SpringBootTest With @MockitoBean
Both setups replace the repository with a mock, and the difference is who creates the objects. With MockitoExtension, Mockito creates both the service and the mock, so Spring is not involved at all.
With @SpringBootTest, Spring starts the whole application context (that is, the container that holds all the objects, or beans, of our application). @MockitoBean then replaces one bean, the repository, with a mock.

| Plain unit test | @SpringBootTest + @MockitoBean | |
|---|---|---|
| Spring context | None | Full application context |
| Service object created by | Mockito (@InjectMocks) | Spring (@Autowired) |
| Time in the example project | Under 1 second for 10 tests | 3 to 5 seconds for 2 tests |
| Stub that the code never uses | Fails with UnnecessaryStubbingException | Allowed |
| Good for | Business logic in services | Bean wiring, transactions, configuration, security |
For service logic, we write plain unit tests first. We start a Spring context only when the test checks something that Spring itself does. For the web layer or the JPA layer, a test slice such as @WebMvcTest or @DataJpaTest is a better choice, because it starts only the beans of that one layer.
2. Spring Boot Mockito Example
The following example is a small song library running on Spring Boot 4.1.1, Java 25, JUnit Jupiter 6.1.3 and Mockito 5.24.0. A SongService reads and writes songs through a Spring Data JPA repository, and 19 tests check the service. The complete project on GitHub runs with mvn test.
2.1. Maven Dependencies
The starter spring-boot-starter-test already brings JUnit Jupiter, AssertJ, mockito-core and mockito-junit-jupiter (the jar that contains MockitoExtension), so a Spring Boot project needs no separate Mockito dependency.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
Spring Boot 4.1.1 manages JUnit Jupiter 6.0.3 and Mockito 5.23.0, which means Spring Boot picks these versions for us. The example moves to the latest releases on Maven Central with two properties, but the managed versions work the same for every snippet in the article.
<properties>
<java.version>25</java.version>
<mockito.version>5.24.0</mockito.version>
<junit-jupiter.version>6.1.3</junit-jupiter.version>
</properties>
The command mvn dependency:tree shows the resolved versions.
\- org.springframework.boot:spring-boot-starter-test:jar:4.1.1:test
+- org.assertj:assertj-core:jar:3.27.7:test
+- org.junit.jupiter:junit-jupiter:jar:6.1.3:test
+- org.mockito:mockito-core:jar:5.24.0:test
| \- net.bytebuddy:byte-buddy-agent:jar:1.18.11:test
\- org.mockito:mockito-junit-jupiter:jar:5.24.0:test
2.2. The Service and Repository Under Test
SongRepository is a Spring Data JPA interface, and Spring Data creates its implementation when the application starts. A unit test never needs that implementation, because the mock takes its place.
public interface SongRepository extends JpaRepository<Song, Long> {
List<Song> findByArtist(String artist);
}
SongService gets the repository through its constructor, which is called constructor injection. Constructor injection lets us test the service without Spring, because @InjectMocks can call the constructor for us, and a test without Mockito could call new SongService(fakeRepository) in the same way.
@Service
public class SongService {
private final SongRepository songRepository;
public SongService(SongRepository songRepository) {
this.songRepository = songRepository;
}
public Song getSong(Long id) {
return songRepository.findById(id)
.orElseThrow(() -> new SongNotFoundException(id)); // "Song 99 not found"
}
public int totalSeconds(String artist) {
return songRepository.findByArtist(artist).stream()
.mapToInt(Song::getSeconds)
.sum();
}
public Song addSong(String title, String artist, int seconds) {
if (seconds <= 0) {
throw new IllegalArgumentException("seconds must be positive");
}
return songRepository.save(new Song(title.trim(), artist.trim(), seconds));
}
public void removeSong(Long id) {
if (!songRepository.existsById(id)) {
throw new SongNotFoundException(id);
}
songRepository.deleteById(id);
}
}
2.3. Stubbing a Repository With when() and thenReturn()
The call when(…).thenReturn(…) tells the mock what to return for one method with one set of arguments, and the service uses that value as if the database had returned it. In the snippet, the mock returns two Adele songs, and totalSeconds() adds up their seconds.
when(songRepository.findByArtist("Adele")).thenReturn(List.of(
new Song(1L, "Hello", "Adele", 295),
new Song(2L, "Skyfall", "Adele", 286)));
int total = songService.totalSeconds("Adele"); // 581
int none = songService.totalSeconds("Nobody"); // 0, the unstubbed call returns an empty list
We never stubbed findByArtist(“Nobody”), yet that call returned an empty list, not an error. A method we did not stub returns a default value. Objects default to null and numbers to 0 (false for boolean), whereas a List, Set, Map or Optional comes back empty. So for a missing song, findById() returns Optional.empty(), and we can also stub that case on purpose and check the exception.
when(songRepository.findById(99L)).thenReturn(Optional.empty());
assertThatThrownBy(() -> songService.getSong(99L))
.isInstanceOf(SongNotFoundException.class)
.hasMessage("Song 99 not found"); // passes
When the returned value depends on the argument, thenAnswer() computes the value at the time of the call. Section 2.6 uses thenAnswer() to return the same Song that the service passed to save().
2.4. Throwing Exceptions With thenThrow() and doThrow()
A real repository can fail, for example when the database rejects a duplicate key or the connection drops. The method thenThrow() makes the mock throw an exception, so we can test how the service reacts. Because SongService does not catch the exception, the test checks that the exception reaches the caller unchanged.
when(songRepository.save(any(Song.class)))
.thenThrow(new DataIntegrityViolationException("duplicate song"));
assertThatThrownBy(() -> songService.addSong("Hello", "Adele", 295))
.isInstanceOf(DataIntegrityViolationException.class)
.hasMessage("duplicate song"); // passes
The method deleteById() returns void, but when(…) needs a method that returns a value, so when(songRepository.deleteById(7L)) does not compile. For a void method, we write the call in reverse order. The call doThrow() comes first, followed by when() with the mock and the method.
when(songRepository.existsById(7L)).thenReturn(true);
doThrow(new DataAccessResourceFailureException("database down"))
.when(songRepository).deleteById(7L);
assertThatThrownBy(() -> songService.removeSong(7L))
.hasMessage("database down"); // passes
In plain JUnit, we write the same check with assertThrows(), which works for all expected exceptions in JUnit.
2.5. Verifying Calls With verify() and verifyNoMoreInteractions()
The method verify() checks that our code called a mock method with the given arguments, and an optional second argument sets how many times the call must happen.
| Call | Passes when the method was called |
|---|---|
| verify(songRepository).deleteById(7L) | Exactly once with 7L (times(1) is the default) |
| verify(songRepository, times(2)).findById(1L) | Exactly twice |
| verify(songRepository, never()).deleteById(any()) | Never, with any argument |
| verify(songRepository, atLeastOnce()).findAll() | One or more times |
| verifyNoInteractions(songRepository) | No method of the mock at all |
| verifyNoMoreInteractions(songRepository) | No method other than the ones we verified before |
In the first part of the snippet, addSong() gets 0 seconds and throws before the database call, so verifyNoInteractions() proves that the service never called the mock. In the second part, verifyNoMoreInteractions() proves that removeSong() calls only the two expected methods.
assertThatThrownBy(() -> songService.addSong("Hello", "Adele", 0))
.hasMessage("seconds must be positive");
verifyNoInteractions(songRepository); // passes, save() was never called
when(songRepository.existsById(7L)).thenReturn(true);
songService.removeSong(7L);
verify(songRepository).existsById(7L);
verify(songRepository, times(1)).deleteById(7L);
verifyNoMoreInteractions(songRepository); // passes
When the code calls a method that the test did not verify, verifyNoMoreInteractions() fails and Mockito lists every call. With MockitoExtension, a stubbed call that the code used counts as verified. In the output, call 1 is existsById(), which the test stubbed, so it has no [?] mark. Only deleteById() on line 37 is unverified, and that call fails the test.
org.mockito.exceptions.verification.NoInteractionsWanted:
No interactions wanted here:
-> at com.howtodoinjava.songs.StrictStubsDemoTest.deleteNotVerified(StrictStubsDemoTest.java:51)
But found this interaction on mock 'songRepository':
-> at com.howtodoinjava.songs.SongService.removeSong(SongService.java:37)
***
For your reference, here is the list of all invocations ([?] - means unverified).
1. -> at com.howtodoinjava.songs.SongService.removeSong(SongService.java:34)
2. [?]-> at com.howtodoinjava.songs.SongService.removeSong(SongService.java:37)
We use verifyNoMoreInteractions() where an extra call is a bug, for example a second save(). We don’t add the check to every test, because otherwise tests start to fail after harmless refactoring.
2.6. Capturing Arguments With ArgumentCaptor
The method addSong() creates a new Song inside the service, so the test never gets a reference to that object and cannot check its fields. An ArgumentCaptor records the argument passed to save() during verify(). Afterwards, we read the recorded Song and check its fields.
@Captor
ArgumentCaptor<Song> songCaptor;
when(songRepository.save(any(Song.class)))
.thenAnswer(invocation -> invocation.getArgument(0)); // return the saved song
songService.addSong(" Hello ", " Adele", 295);
verify(songRepository).save(songCaptor.capture());
Song saved = songCaptor.getValue();
String title = saved.getTitle(); // "Hello", trimmed by the service
String artist = saved.getArtist(); // "Adele"
int seconds = saved.getSeconds(); // 295
The method getValue() returns the last captured argument. When the method ran more than once, getAllValues() returns all of them, so we can verify multiple method arguments across several calls.
2.7. Writing Tests in BDD Style With BDDMockito
BDD stands for behavior-driven development, where a test reads in three parts, namely given, when and then. BDDMockito has the same features as Mockito, and only the method names change to match the three BDD words.
| Mockito | BDDMockito |
|---|---|
| when(mock.call()).thenReturn(x) | given(mock.call()).willReturn(x) |
| when(mock.call()).thenThrow(e) | given(mock.call()).willThrow(e) |
| doThrow(e).when(mock).voidCall() | willThrow(e).given(mock).voidCall() |
| verify(mock).call() | then(mock).should().call() |
| verify(mock, never()).call() | then(mock).should(never()).call() |
| verifyNoMoreInteractions(mock) | then(mock).shouldHaveNoMoreInteractions() |
In BDD style, the totalSeconds() test from section 2.3 reads as given, when and then steps.
// given
given(songRepository.findByArtist("Adele")).willReturn(List.of(
new Song(1L, "Hello", "Adele", 295),
new Song(2L, "Skyfall", "Adele", 286)));
// when
int total = songService.totalSeconds("Adele"); // 581
// then
assertThat(total).isEqualTo(581);
then(songRepository).should().findByArtist("Adele");
then(songRepository).shouldHaveNoMoreInteractions();
Both styles create the same stubs, and one test class can mix them. Still, we pick one style per project so that all tests read the same way.
2.8. Mocking a Bean in @SpringBootTest With @MockitoBean
@MockitoBean comes from Spring Framework, not from Spring Boot, and is in the package org.springframework.test.context.bean.override.mockito. In SongServiceSpringBootTest, the annotation replaces the SongRepository bean in the application context with a mock. Every bean that needs the repository gets the mock instead, so SongService holds the mock.
@SpringBootTest
class SongServiceSpringBootTest {
@MockitoBean
SongRepository songRepository; // replaces the Spring Data repository bean
@Autowired
SongService songService; // real service bean, holds the mock
@Test
void serviceBeanUsesMockRepository() {
when(songRepository.findById(1L))
.thenReturn(Optional.of(new Song(1L, "Yesterday", "The Beatles", 125)));
String title = songService.getSong(1L).getTitle(); // "Yesterday"
verify(songRepository).findById(1L); // passes
String mockClass = songRepository.getClass().getSimpleName(); // SongRepository$MockitoMock$QXzzfSwi (suffix differs per run)
}
}
Because only the repository bean is replaced, the context still starts the HikariCP connection pool, Hibernate and the H2 database.
c.h.songs.SongServiceSpringBootTest : Starting SongServiceSpringBootTest using Java 25.0.4.1
.s.d.r.c.RepositoryConfigurationDelegate : Found 1 JPA repository interface.
com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
org.hibernate.orm.core : HHH000001: Hibernate ORM core version 7.4.5.Final
c.h.songs.SongServiceSpringBootTest : Started SongServiceSpringBootTest in 9.056 seconds (process running for 16.993)
By default (reset = MockReset.AFTER), Spring resets each @MockitoBean after every test method, so stubs from one test do not affect the next test.
Spring also caches the context and reuses it for test classes with the same configuration. A different set of @MockitoBean fields counts as a new configuration, so Spring creates a new context. Other @MockitoBean options include @MockitoBean on the class level and @MockitoSpyBean.
@MockBean and @SpyBean from org.springframework.boot.test.mock.mockito were deprecated in Spring Boot 3.4, and Spring Boot 4.0 removed both annotations. An old test then fails to compile with package org.springframework.boot.test.mock.mockito does not exist. To fix it, we change the imports to @MockitoBean and @MockitoSpyBean from org.springframework.test.context.bean.override.mockito, while the field declarations stay the same. Projects on Spring Boot 3.3 and earlier still use @MockBean.
2.9. Strict Stubs and UnnecessaryStubbingException
MockitoExtension runs every test with strict stubs (Strictness.STRICT_STUBS), which means Mockito also checks how the test uses its stubs. A stub that the code never uses fails the test. An unused stub often means the test does not test what we think. In the next test, getSong() never calls existsById(), so the first stub is unused.
when(songRepository.existsById(1L)).thenReturn(true); // never used by getSong()
when(songRepository.findById(1L))
.thenReturn(Optional.of(new Song(1L, "Yesterday", "The Beatles", 125)));
songService.getSong(1L);
org.mockito.exceptions.misusing.UnnecessaryStubbingException:
Unnecessary stubbings detected.
Clean & maintainable test code requires zero unnecessary code.
Following stubbings are unnecessary (click to navigate to relevant line of code):
1. -> at com.howtodoinjava.songs.StrictStubsDemoTest.unusedStub(StrictStubsDemoTest.java:30)
Please remove unnecessary stubbings or use 'lenient' strictness. More info: javadoc for UnnecessaryStubbingException class.
Strict stubs also catch a stub with the wrong argument. In the next case, the test stubs findById(1L) but the code calls findById(2L), so Mockito fails right at that call instead of returning an empty Optional.
org.mockito.exceptions.misusing.PotentialStubbingProblem:
Strict stubbing argument mismatch. Please check:
- this invocation of 'findById' method:
songRepository.findById(2L);
-> at com.howtodoinjava.songs.SongService.getSong(SongService.java:16)
- has following stubbing(s) with different arguments:
1. songRepository.findById(1L); stubbed with: [Returns: Optional[Song[id=1, title=Yesterday, artist=The Beatles, seconds=125]]]
-> at com.howtodoinjava.songs.StrictStubsDemoTest.stubWithOtherArgument(StrictStubsDemoTest.java:39)
The first fix is to delete the unused stub or to correct the argument. When a stub is needed only in some tests, for example in a shared @BeforeEach method, we mark that one stub as lenient, so it may stay unused without an error.
lenient().when(songRepository.existsById(1L)).thenReturn(true); // unused, no error
@MockitoSettings(strictness = Strictness.LENIENT) turns strict stubs off for a whole class. We avoid the lenient setting, because the wrong-argument mistake then passes without a Mockito error. With LENIENT, findById(2L) returns the default Optional.empty(), so the service throws SongNotFoundException, and no Mockito error names the wrong argument.
2.10. Loading Mockito as a Java Agent on Java 21 and Later
Mockito 5 creates mocks with the inline mock maker, which changes classes while the tests run and needs a Java agent (a jar that the JVM allows to change classes) for that. Without extra setup, Mockito attaches this agent to the running JVM by itself. Since Java 21 (JEP 451), the JVM warns about agents loaded this way, and mvn test on Java 25 prints a Mockito notice and four WARNING lines.
Mockito is currently self-attaching to enable the inline-mock-maker. This will no longer work in future releases of the JDK. Please add Mockito as an agent to your build as described in Mockito's documentation: https://javadoc.io/doc/org.mockito/mockito-core/latest/org.mockito/org/mockito/Mockito.html#0.3
WARNING: A Java agent has been loaded dynamically (/root/.m2/repository/net/bytebuddy/byte-buddy-agent/1.18.11/byte-buddy-agent-1.18.11.jar)
WARNING: If a serviceability tool is in use, please run with -XX:+EnableDynamicAgentLoading to hide this warning
WARNING: If a serviceability tool is not in use, please run with -Djdk.instrument.traceUsage for more information
WARNING: Dynamic loading of agents will be disallowed by default in a future release
The tests still pass today, but a future JDK will refuse the dynamic agent, and the mocks will stop working. So we load Mockito as an agent when the test JVM starts, using two Maven plugins.
- The maven-dependency-plugin goal properties creates one property per dependency, named groupId:artifactId:type, whose value is the path of the jar. So the Mockito jar path is in org.mockito:mockito-core:jar.
- The maven-surefire-plugin runs the tests in a separate (forked) JVM and passes -javaagent with that path to it.
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>properties</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<argLine>-javaagent:@{org.mockito:mockito-core:jar}</argLine>
</configuration>
</plugin>
@{…} is Surefire’s late property replacement, so Surefire reads the property when the tests start, after the properties goal has set it.
The Mockito setup uses the normal Maven property syntax and also starts argLine with @{argLine}. We add @{argLine} only when another plugin, such as JaCoCo, sets the argLine property, because without JaCoCo or a similar plugin, the property is undefined and the build fails before any test runs.
Error: could not open `{argLine}'
With the agent loaded at startup, the Mockito line and the four WARNING lines are gone, and all 19 tests pass. Both builds, with and without the agent, also printed the JVM line OpenJDK 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended, which is a class-data sharing notice and does not affect the tests.
2.11. Running the Tests
The command mvn test runs all test classes. One class, StrictStubsDemoTest, fails on purpose to produce the errors in section 2.9, so the build skips the JUnit tag fails-on-purpose. A separate class, StrictStubsTest, runs the demo class and checks the exception types.
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.740 s -- in com.howtodoinjava.songs.LenientSettingsTest
[INFO] Tests run: 10, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.111 s -- in com.howtodoinjava.songs.SongServiceTest
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 3.027 s -- in com.howtodoinjava.songs.SongServiceSpringBootTest
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.023 s -- in com.howtodoinjava.songs.SongServiceBddTest
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.053 s -- in com.howtodoinjava.songs.StrictStubsTest
[INFO] Tests run: 19, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS
The 10 plain unit tests of SongServiceTest took 0.111 seconds, whereas the 2 tests of SongServiceSpringBootTest took 3.027 seconds, almost all of it for starting the context. To see the failing demo tests, we run mvn test -Dtest=StrictStubsDemoTest -DexcludedGroups=none.
3. Spring Boot Mockito FAQs
3.1. What Is the Difference Between @Mock and @MockitoBean?
@Mock creates a mock in a plain unit test, and Spring does not know about that mock. @MockitoBean also creates a mock, but it registers the mock as a bean in the Spring application context, where it replaces the bean of the same type.
So we use @Mock with MockitoExtension for service tests, and @MockitoBean with @SpringBootTest, @WebMvcTest or another Spring test.
3.2. Do We Need MockitoAnnotations.openMocks()?
No, not with @ExtendWith(MockitoExtension.class). The extension creates the @Mock, @Captor and @InjectMocks fields before each test and checks the strict stubs after each test.
MockitoAnnotations.openMocks(this) is the manual way for a test class without the extension. We call it in a @BeforeEach method, and since it returns an AutoCloseable, we close that in @AfterEach. The older MockitoAnnotations.initMocks() is deprecated. Either way, the Mockito annotations work the same.
3.3. Why Does @MockitoBean Not Report Unnecessary Stubbing?
Because @SpringBootTest does not use MockitoExtension, the strict stubs check never runs. That’s why our SongServiceSpringBootTest has a test with an unused existsById() stub, and that test passes. Calls to verify() and ArgumentCaptor still work the same as in a plain unit test.
3.4. Should We Mock the Repository or Test It With a Database?
Both, in different tests. A service test mocks the repository, because the test checks the service’s own logic, such as validation and trimming. The repository’s queries need a real database, because a mock returns whatever we stub, even for a query with a wrong column name.
@DataJpaTest tests the repository against an embedded database or a Testcontainers database, and an integration test checks all layers together.
3.5. Why Do We Get “Could Not Initialize Plugin MockMaker”?
The inline mock maker could not attach its agent. Two common reasons are an unsupported JDK and an old version of Byte Buddy (the library that Mockito uses to generate mock classes) on the classpath. Loading Mockito as a Java agent at JVM start, as in section 2.10, removes the self-attach step that fails with the MockMaker error.
4. Conclusion
A service unit test needs @ExtendWith(MockitoExtension.class) to turn on Mockito. Inside the class, @Mock fakes the repository and @InjectMocks builds the service.
We stub return values with when().thenReturn() and failures with thenThrow(), or with doThrow() for void methods. We check calls with verify(), and with verifyNoInteractions() or verifyNoMoreInteractions() when an extra call is a bug.
An ArgumentCaptor reads objects that the service creates inside itself, and BDDMockito offers the same features in given / then style. @MockitoBean replaces a bean in a Spring context and is the Spring Boot 4 replacement for @MockBean. Strict stubs reject unused stubs and stubs with wrong arguments, and on Java 21 and later, we load Mockito with -javaagent in Surefire.
5. References
- Mockito JavaDoc
- Mockito Strictness JavaDoc
- Spring Framework Reference: @MockitoBean and @MockitoSpyBean
- Spring Boot Reference: Testing Spring Boot Applications
- Spring Boot 3.5 API: @MockBean (deprecated)
- JUnit User Guide
- Maven Dependency Plugin: properties goal
- JEP 451: Prepare to Disallow the Dynamic Loading of Agents
Happy Learning !!