Test pyramid, JUnit 5, AssertJ, Mockito, test doubles, TDD, Spring Boot test slices, Testcontainers, contract testing, flaky tests, concurrency and time testing, coverage and mutation testing.
Theory
Q1
What is the test pyramid and why does it matter?
basic
The pyramid says to have many fast, isolated unit tests, fewer integration tests and very few end-to-end tests. Cost, run time and flakiness grow as you go up, while precision of failure location drops.
Unit: one class, collaborators doubled, milliseconds.
Integration: real wiring with DB, HTTP or messaging, seconds.
E2E/UI: whole deployed system, slow and brittle.
Variants: the "testing trophy" favours integration tests; the "ice cream cone" (mostly manual/E2E) is the anti-pattern.
⚠ Follow-up traps
Is the pyramid a fixed ratio? No, it is a heuristic about cost and feedback speed. Microservices often shift weight toward integration and contract tests.
Where do contract tests sit? Between integration and E2E, replacing many cross-service E2E tests.
#test-pyramid#strategy
Q2
What distinguishes a unit test from an integration test?
basic
A unit test exercises one unit in isolation with no I/O, no network and no framework startup. An integration test verifies that several real components cooperate, such as a repository with a real database.
Unit tests should run in-process in milliseconds and be deterministic.
"Unit" is defined by behaviour, not necessarily one class; the London school mocks collaborators, the Detroit/classicist school uses real ones when cheap.
Maven convention: Surefire runs *Test, Failsafe runs *IT in the verify phase.
⚠ Follow-up traps
Is a test that uses @SpringBootTest a unit test? No, it loads a context and is an integration test even if it mocks everything.
Does one class per test make it a unit test? Not if it hits a real DB or the clock.
#unit-test#integration-test
Q3
Describe the JUnit 5 architecture.
basic
JUnit 5 = JUnit Platform (launches tests, TestEngine SPI) + JUnit Jupiter (the new programming and extension model) + JUnit Vintage (runs JUnit 3/4 tests on the platform).
Package is org.junit.jupiter.api; tests may be package-private.
Other engines (Spock, ArchUnit, Cucumber) plug in through the same platform.
Requires Java 8+; Spring Boot 3 starters bring Jupiter by default and no longer ship Vintage.
⚠ Follow-up traps
Do JUnit 4 @RunWith and @Rule work in Jupiter? No. Use @ExtendWith and extensions; Vintage engine is needed to run old tests.
Must test classes and methods be public? No, package-private is enough and preferred.
#junit5#architecture
Q4
Explain the JUnit 5 lifecycle annotations and their order.
basic
@BeforeAll once before all tests, @BeforeEach before every test, then the test, @AfterEach, and finally @AfterAll. A new test class instance is created per test method by default.
@BeforeAll/@AfterAll must be static unless @TestInstance(Lifecycle.PER_CLASS) is used.
Superclass @BeforeEach runs before the subclass one; @AfterEach runs in reverse.
Does instance state leak between tests by default? No, each test gets a fresh instance; with PER_CLASS it does leak.
Can @BeforeAll be non-static? Only with PER_CLASS lifecycle.
#junit5#lifecycle
Q5
How do assertAll and assertThrows work?
basic
assertAll runs all grouped assertions and reports every failure together instead of stopping at the first. assertThrows(Type.class, executable) passes if the executable throws that type or a subtype and returns the exception for further checks.
@Testvoid rejectsNegativeAmount() { var ex = assertThrows(IllegalArgumentException.class, () -> new Money(-1, "USD")); assertAll("money error", () -> assertTrue(ex.getMessage().contains("negative")), () -> assertNull(ex.getCause()));}
assertThrowsExactly demands the exact type, not a subtype.
assertTimeout waits for completion; assertTimeoutPreemptively aborts in another thread.
⚠ Follow-up traps
Does assertThrows(Exception.class, ...) accept a RuntimeException? Yes, subtypes pass. Use assertThrowsExactly for strictness.
Argument order of assertEquals?(expected, actual); swapping gives misleading failure messages.
#junit5#assertions
Q6
How do parameterized tests work in JUnit 5?
intermediate
@ParameterizedTest plus a source annotation runs the same method with many inputs, each as its own reported invocation. It needs the junit-jupiter-params artifact (included in junit-jupiter).
@MethodSource factory is static (unless PER_CLASS) and returns Stream<Arguments>.
name = "{index}: {0} -> {1}" customizes display names.
Implicit conversion handles String to enum, numbers, dates; @ConvertWith for custom.
⚠ Follow-up traps
Can @ValueSource supply multiple arguments? No, one argument per invocation; use @CsvSource or @MethodSource.
Does a failing row stop the others? No, each invocation is independent.
#junit5#parameterized
Q7
What is a JUnit 5 extension and how does it compare to JUnit 4 runners and rules?
intermediate
Extensions implement callback interfaces such as BeforeEachCallback, ParameterResolver, TestExecutionExceptionHandler and ExecutionCondition, and are registered with @ExtendWith. Unlike JUnit 4, you can combine many extensions on one class.
Registration: declarative @ExtendWith, programmatic @RegisterExtension field, or automatic via ServiceLoader (enable junit.jupiter.extensions.autodetection.enabled).
MockitoExtension, SpringExtension, @TempDir and TestInfo injection all use this model.
Use the ExtensionContext.Store for per-test or per-class state.
⚠ Follow-up traps
Can you have two @RunWith runners in JUnit 4? No, only one; that limitation is why extensions exist.
Is extension order guaranteed? Declared order via @ExtendWith; with @RegisterExtension use @Order.
#junit5#extensions
Q8
How do you write a custom ParameterResolver?
advanced
Implement ParameterResolver with supportsParameter and resolveParameter, then register it. JUnit calls it for constructor and method parameters that it does not resolve itself.
class RandomPortResolver implements ParameterResolver { public boolean supportsParameter(ParameterContext p, ExtensionContext c) { return p.getParameter().getType() == int.class && p.isAnnotated(RandomPort.class); } public Object resolveParameter(ParameterContext p, ExtensionContext c) { try (var s = new java.net.ServerSocket(0)) { return s.getLocalPort(); } catch (java.io.IOException e) { throw new RuntimeException(e); } }}
⚠ Follow-up traps
What if two resolvers support the same parameter? JUnit throws ParameterResolutionException for ambiguity.
Can it inject into @BeforeEach methods? Yes, any lifecycle or test method.
#junit5#extensions#parameter-resolver
Q9
What are @Nested tests and when are they useful?
intermediate
@Nested marks a non-static inner class as a group of tests that shares the outer context, so you can express "given X, when Y, then Z" hierarchies with layered @BeforeEach setup.
Outer @BeforeEach runs before the inner one for each inner test.
Inner classes must be non-static; static members were not allowed before Java 16.
Pair with @DisplayName for readable reports.
⚠ Follow-up traps
Does @BeforeAll work in a @Nested class? Only with PER_CLASS lifecycle (or static members on Java 16+).
Does a nested class see outer fields? Yes, it holds a reference to the outer instance.
#junit5#nested#structure
Q10
How do tags, conditions and disabling tests work in JUnit 5?
basic
@Tag("slow") labels tests so build tools can include or exclude them. Conditional annotations such as @EnabledOnOs, @EnabledIfEnvironmentVariable, @EnabledForJreRange and @DisabledIf toggle execution, and @Disabled skips unconditionally.
Assumptions (assumeTrue) abort a test, reported as skipped, not failed.
Always put a reason in @Disabled("JIRA-123").
⚠ Follow-up traps
Failed assumption vs failed assertion? Assumption aborts (skipped), assertion fails.
Is @Ignore still valid? It is JUnit 4 only; Jupiter uses @Disabled.
#junit5#tags#conditions
Q11
What are @TempDir, TestInfo and @RepeatedTest used for?
intermediate
@TempDir injects a temporary directory deleted after the test, TestInfo gives display name and tags, and @RepeatedTest(n) re-runs a test n times with RepetitionInfo.
@TempDir works on fields (per class or per test) and parameters, typed Path or File.
@RepeatedTest is useful when diagnosing flaky behaviour, not as a cure.
@TestMethodOrder(OrderAnnotation.class) exists, but order dependence is a smell.
⚠ Follow-up traps
Is the temp dir shared between tests? Not for a parameter or instance field; a static field shares one per class.
Are test methods run in a fixed order? Deterministic but intentionally non-obvious by default.
#junit5#temp-dir#repeated-test
Q12
What is @TestInstance and when should you change the default?
intermediate
By default (PER_METHOD) JUnit creates a new instance for each test. @TestInstance(PER_CLASS) creates one instance per class, allowing non-static @BeforeAll/@AfterAll and @MethodSource factories.
Change it for expensive shared setup or when using Kotlin or @Nested with @BeforeAll.
Cost: instance fields become shared mutable state, so tests may couple.
Set globally with junit.jupiter.testinstance.lifecycle.default=per_class.
⚠ Follow-up traps
Does PER_CLASS make tests run in parallel? No, parallelism is configured separately.
Is a field reset between tests under PER_CLASS? No, you must reset it in @BeforeEach.
#junit5#lifecycle#test-instance
Q13
How does parallel test execution work in JUnit 5?
advanced
Enable junit.jupiter.execution.parallel.enabled=true, then choose mode (same_thread or concurrent) per class or method; the platform runs them on a ForkJoinPool strategy. Use @ResourceLock for shared resources and @Execution to opt in/out.
Config via junit-platform.properties.
Static state, shared DB rows, files and ports are the usual causes of failures once parallel.
Surefire forkCount / parallel is a separate, process-level mechanism.
⚠ Follow-up traps
Is @BeforeAll thread-safe automatically? It runs once per class, but shared static state touched by tests still needs protection.
Does Spring's context cache help parallel runs? Contexts are cached and shared, so tests mutating context state can interfere.
#junit5#parallel
Q14
Why use AssertJ instead of plain JUnit assertions?
basic
AssertJ offers a fluent, discoverable API with rich failure messages: assertThat(actual).isEqualTo(...) with chained type-specific checks for strings, collections, optionals, dates and exceptions.
usingRecursiveComparison() compares objects field by field without equals.
SoftAssertions / assertSoftly collects failures like assertAll.
Spring Boot starter-test ships AssertJ and Hamcrest.
⚠ Follow-up traps
Does assertThat(x) without a terminal check fail? No, it silently passes; a dangling assertThat(x) verifies nothing.
containsExactly vs containsExactlyInAnyOrder? First checks order too.
#assertj#assertions
Q15
How does recursive comparison in AssertJ differ from equals?
intermediate
usingRecursiveComparison() compares fields reflectively, ignoring the class's equals, so it works on types that do not override it and reports the exact differing path.
Configure with ignoringFields, ignoringFieldsMatchingRegexes, comparingOnlyFields, withComparatorForType.
By default, differing types with identical fields are considered equal unless withStrictTypeChecking().
Useful for DTO/entity mapping tests with generated ids or timestamps.
⚠ Follow-up traps
Does it call equals on nested objects? Not by default; use usingOverriddenEquals() to change that.
How to ignore generated IDs?ignoringFields("id", "createdAt").
#assertj#recursive-comparison
Q16
List the types of test doubles.
basic
Gerard Meszaros' taxonomy: dummy, stub, spy, mock and fake.
Dummy: passed to fill a parameter, never used.
Stub: returns canned answers.
Spy: a real or recording object that remembers calls.
Mock: pre-programmed with expectations and verifies interactions.
Fake: working lightweight implementation (in-memory repository, H2).
Mockito calls everything a "mock", but a stubbed-only mock acts as a stub.
⚠ Follow-up traps
Is an H2 in-memory DB a fake or a mock? A fake, a real but simplified implementation.
Stub vs mock verification? Stubs support state verification; mocks verify behaviour (calls).
#test-doubles#dummy#stub#fake
Q17
State vs behaviour verification: which should you prefer?
intermediate
Prefer asserting observable outcomes (return values, state, persisted data) over verifying internal calls. Behaviour verification is justified for side effects that have no observable result, such as sending an email.
Over-verifying couples tests to implementation, so refactors break tests.
Query methods should be stubbed, commands should be verified.
Fakes give state-based tests without brittle verify chains.
⚠ Follow-up traps
Should every mock call be verified? No; verifying stubbed query calls is redundant and brittle.
Is verifyNoMoreInteractions a good default? Usually not; it makes tests fragile.
#test-doubles#design#mockist
Q18
What is the difference between mock and spy in Mockito?
basic
A mock replaces all behaviour: methods return defaults (null, 0, empty collections/Optional) unless stubbed. A spy wraps a real object, calling real methods unless stubbed.
spy(realObject) copies the instance state at creation; the spy is a different object from the original.
Stub spies with doReturn(x).when(spy).method(), because when(spy.method()) invokes the real method first.
A spy signals a design smell (partial mocking); prefer extracting a collaborator.
⚠ Follow-up traps
Does mutating the original object after spy(original) affect the spy? No, the spy is a copy.
Why doReturn for spies?when(spy.m()) executes the real m(), which may throw or have side effects.
#mockito#mock#spy
Q19
How do you use @Mock, @InjectMocks and MockitoExtension?
basic
@ExtendWith(MockitoExtension.class) initializes @Mock and @Spy fields and builds the @InjectMocks object, trying constructor injection first, then setter, then field injection by type and name.
Failure to inject is silent when the strategy fails (a field stays null); prefer explicit constructor creation when in doubt.
JUnit 4 equivalent: MockitoJUnitRunner or MockitoAnnotations.openMocks.
⚠ Follow-up traps
If constructor injection cannot find a mock for a parameter? Mockito passes null for that parameter.
Does @InjectMocks use field injection after constructor injection? No, it picks the first successful strategy.
#mockito#injectmocks#junit5
Q20
How do stubbing styles differ: when/thenReturn, doReturn, BDDMockito?
intermediate
when(m.call()).thenReturn(x) is the common form. doReturn/doThrow/doNothing/doAnswer().when(m).call() is required for void methods and spies. BDDMockito.given(...).willReturn(...) and then(m).should() are aliases with given-when-then wording.
Chain consecutive results: thenReturn(a, b, c) or thenThrow(...).
thenAnswer computes from arguments.
Later stubbing of the same call overrides earlier.
⚠ Follow-up traps
Can when(voidMethod()) compile? No; use doThrow(...).when(mock).voidMethod().
Which stub wins when two overlap? The last one declared for matching args.
#mockito#stubbing#bdd
Q21
How do argument matchers work and what is the all-or-nothing rule?
intermediate
Matchers such as any(), eq(), anyString(), argThat() and isNull() match arguments flexibly. If one argument uses a matcher, all arguments in that call must use matchers; wrap raw values with eq().
Mockito 2+: any(Class) is type-checking and does not match null; any() matches anything including null.
anyString() does not match null in Mockito 2+.
⚠ Follow-up traps
Does anyString() match null? No (Mockito 2+); use any() or isNull().
Can you extract a matcher into a variable? No, matchers register on call; extract into a method that returns the matcher call instead.
#mockito#matchers
Q22
How does verify work, and what do times/never/inOrder do?
intermediate
verify(mock).method(args) checks exactly one call with matching arguments. Modes: times(n), never(), atLeast(n), atMost(n), timeout(ms). InOrder checks relative order across mocks.
InOrder order = inOrder(audit, repo);order.verify(audit).start();order.verify(repo).save(any());verifyNoMoreInteractions(repo);
verifyNoInteractions(mock) asserts the mock was never touched.
verify after the fact does not require stubbing.
⚠ Follow-up traps
Does plain verify(m).x() accept two calls? No, default is times(1) and fails on two.
Does inOrder require verifying every call? No, only the ones you list, in relative order.
#mockito#verify
Q23
What does ArgumentCaptor do and when is it better than a matcher?
intermediate
ArgumentCaptor captures the actual argument passed to a mock so you can assert on its fields, which is better than argThat when the object is complex or built inside the code under test.
var captor = ArgumentCaptor.forClass(Email.class);verify(mailer).send(captor.capture());assertThat(captor.getValue().to()).isEqualTo("a@x.com");
@Captor annotation works with the extension.
getAllValues() returns every capture across calls.
Do not capture inside stubbing (when); use it with verify.
⚠ Follow-up traps
Is the captured object a copy? No, it is the same reference; later mutation changes it.
Does capture() work as a stub matcher? Technically yes but it is discouraged by Mockito docs.
#mockito#argumentcaptor
Q24
What are Mockito strict stubs and UnnecessaryStubbingException?
intermediate
With MockitoExtension the default strictness is STRICT_STUBS: an unused stub fails the test with UnnecessaryStubbingException, and a stubbed call made with different arguments raises PotentialStubbingProblem.
Keeps tests clean by removing dead setup.
Relax per stub with lenient().when(...) or per class via @MockitoSettings(strictness = Strictness.LENIENT).
Strict stubs also make verify of stubbed calls redundant.
⚠ Follow-up traps
Does strictness apply when using mock() without the extension? No, only with the extension/rule/session.
Why lenient() in shared @BeforeEach? Not all tests use every stub; otherwise the unused ones fail.
#mockito#strictness#strict-stubs
Q25
What are Mockito's default answers and deep stubs?
advanced
Default answer RETURNS_DEFAULTS gives null, 0, false, empty collections and empty Optional/Stream. RETURNS_DEEP_STUBS returns mocks of return types to allow chained stubbing like when(a.b().c()).thenReturn(x).
RETURNS_SMART_NULLS returns a descriptive exception-throwing object instead of null.
Deep stubs usually signal a Law of Demeter violation; use sparingly (builders, fluent APIs).
Custom Answer for dynamic responses.
⚠ Follow-up traps
What does a mocked List<String> method return by default? An empty list, not null.
Do deep stubs work with generics? Partly; generic return types may need explicit handling.
#mockito#answers#deep-stubs
Q26
Can Mockito mock final classes, static methods and constructors?
advanced
Yes, with the inline mock maker, which is the default since Mockito 5. It uses instrumentation (Byte Buddy agent) to mock final classes/methods, mockStatic and mockConstruction.
try (var s = mockStatic(UUID.class)) { s.when(UUID::randomUUID).thenReturn(FIXED); assertEquals(FIXED, UUID.randomUUID());}
Static mocks are thread-local and must be closed (try-with-resources) or they leak into other tests.
Mockito 4 and earlier: add mockito-inline artifact.
Java 21+ warns about dynamic agent loading; configure -javaagent for Mockito explicitly in future-proof builds.
Prefer wrapping statics behind an injectable interface.
⚠ Follow-up traps
Is a static mock visible to other threads? No, it is only active in the thread that created it.
What if you forget to close MockedStatic? Registering again throws, and later tests may see the mock.
#mockito#static#final#inline-mock-maker
Q27
Why can't Mockito mock equals/hashCode, and what else cannot be stubbed?
advanced
Mockito cannot stub or verify equals() and hashCode(), as it relies on them internally. Private methods are not mockable, and native methods and some JDK internals are restricted even with the inline maker.
Constructor logic of a mocked class is skipped (objenesis), so field initializers do not run.
Mocking value objects, collections or String is a smell: use real instances.
Don't mock types you don't own; wrap them in an adapter and test the adapter via integration test.
⚠ Follow-up traps
Are field initializers of a mocked class executed? No, objenesis bypasses constructors.
Should you mock List? Almost never; use a real list.
#mockito#limitations
Q28
What is TDD and what is the red-green-refactor cycle?
basic
Test-driven development writes a failing test first (red), writes the minimal code to pass (green), then cleans up with tests as the safety net (refactor).
Costs: learning curve, poor fit for exploratory spikes or heavy UI work.
Related: BDD (given-when-then, behaviour naming), ATDD (acceptance test first).
⚠ Follow-up traps
Does TDD mean 100% coverage? No, coverage is a by-product, not the goal.
Why must you see the test fail first? To prove the test can fail and tests the intended behaviour.
#tdd#red-green-refactor
Q29
Classicist versus mockist (London) TDD: what is the difference?
intermediate
The Detroit/classicist style uses real collaborators where cheap and asserts on state. The London/mockist style mocks collaborators and drives design outside-in by verifying interactions.
Mockist gives fast isolated tests and discovers interfaces early, but is more coupled to implementation.
Classicist tests survive refactoring better but failures point to a wider area.
Most teams mix: fakes or real objects for domain, mocks at architectural boundaries.
⚠ Follow-up traps
Which is "correct"? Neither; choose per boundary.
When is mocking a domain object harmful? When it forces tests to mirror internal call sequences.
#tdd#london-school#classicist
Q30
What makes a good unit test (FIRST, AAA, naming)?
basic
FIRST: Fast, Independent, Repeatable, Self-validating, Timely. Structure with Arrange-Act-Assert (or Given-When-Then), test one behaviour, and name the test by behaviour, such as rejectsExpiredToken.
No logic (loops, conditionals) in tests.
One logical assertion; assertAll groups related checks.
Test public behaviour, not private methods.
Avoid shared mutable fixtures and order dependence.
⚠ Follow-up traps
Should you test private methods? No, test them through public behaviour; extract a class if complex.
Is "one assert per test" literal? No, one concept; several asserts on the same outcome are fine.
#best-practices#aaa#first
Q31
What is the test data builder pattern and how does it compare with Object Mother?
intermediate
A test data builder provides a fluent object with valid defaults where each test overrides only what matters. An Object Mother is a factory with named canned instances (Users.admin()), which explode in variants over time.
class UserBuilder { private String name = "Ann"; private int age = 30; UserBuilder age(int a) { this.age = a; return this; } User build() { return new User(name, age); }}// new UserBuilder().age(17).build()
Keeps tests resilient when constructors change (one place to fix).
Why not Lombok @Builder in production class for tests? It forces a production API for tests and lacks valid defaults.
Should defaults be random? Prefer deterministic defaults, optionally random with logged seed.
#test-data-builder#object-mother
Q32
What does @SpringBootTest do, and what are its costs?
basic
@SpringBootTest bootstraps the full ApplicationContext (finds @SpringBootApplication) for integration tests. webEnvironment selects MOCK (default, MockMvc), RANDOM_PORT, DEFINED_PORT or NONE.
Slow startup, so contexts are cached by configuration key across test classes.
@MockBean / @SpyBean (Spring Boot 3.4+: @MockitoBean / @MockitoSpyBean) alter the cache key and force new contexts.
Use TestRestTemplate, WebTestClient or @LocalServerPort with a real port.
⚠ Follow-up traps
Does RANDOM_PORT start a real server? Yes, an embedded server on a random port.
Why do many @MockBean variations slow the build? Each distinct set creates a new cached context.
#spring-boot#springboottest
Q33
What does @WebMvcTest load, and how do you use it?
intermediate
@WebMvcTest(Controller.class) loads only the MVC slice: controllers, @ControllerAdvice, converters, filters and security config for web, but not @Service, @Repository or @Component beans. Collaborators are supplied via @MockBean/@MockitoBean, and MockMvc is auto-configured.
Does it test serialization and validation? Yes, Jackson and Bean Validation run since the MVC stack is real.
Why NoSuchBeanDefinitionException for a service? The slice does not scan services; mock them.
#spring-boot#webmvctest#mockmvc
Q34
What is @DataJpaTest and what does it configure?
intermediate
@DataJpaTest loads JPA repositories, entities, EntityManager, TestEntityManager and Flyway/Liquibase, replaces the DataSource with an embedded DB, and wraps each test in a transaction that rolls back.
Use @AutoConfigureTestDatabase(replace = NONE) with Testcontainers to run on the real DB engine.
Services and controllers are not loaded.
Rollback hides flush issues: call entityManager.flush() and clear() to force SQL and re-read.
⚠ Follow-up traps
Why does an H2 test pass but production fails? Dialect and feature differences; test against the real engine via Testcontainers.
Does the test-level transaction commit? No, it rolls back unless @Commit.
#spring-boot#datajpatest#jpa
Q35
Which other Spring Boot test slices exist?
intermediate
@JsonTest, @WebFluxTest, @DataJdbcTest, @DataMongoTest, @DataRedisTest, @JdbcTest, @RestClientTest and @JooqTest each load only the relevant auto-configuration.
Slices are faster and expose wiring mistakes in the layer under test.
Add extra beans with @Import.
Custom slice annotations combine @TypeExcludeFilters and @AutoConfigure....
⚠ Follow-up traps
Does @RestClientTest start a server? No, it uses MockRestServiceServer to simulate responses.
Do slices share contexts? Only with identical configuration, so each slice type usually has its own.
#spring-boot#slices
Q36
How do @MockBean and @Mock differ?
intermediate
@Mock creates a plain Mockito mock outside Spring. @MockBean creates a mock and registers it in the Spring context, replacing any bean of that type. Boot 3.4 deprecates it in favour of @MockitoBean (Spring Framework 6.2).
@MockBean mocks are reset after each test.
Different @MockBean sets produce different cache keys, so contexts are recreated.
Prefer plain @Mock with constructor injection for unit tests to avoid Spring entirely.
⚠ Follow-up traps
Can @Mock be injected into a Spring bean automatically? No, only @MockBean/@MockitoBean participate in the context.
Does @MockBean work on a bean already proxied? Yes but proxying and AOP effects need care.
#spring-boot#mockbean#mockito
Q37
How do you test Spring Security with MockMvc?
intermediate
Use spring-security-test: @WithMockUser(roles = "ADMIN"), @WithUserDetails, or request post-processors such as jwt(), httpBasic() and csrf().
@WebMvcTest includes the security filter chain; state-changing requests need .with(csrf()) unless CSRF is disabled.
Test both allowed and denied paths (401 vs 403).
Method security (@PreAuthorize) needs a context that enables it, not a plain unit test.
⚠ Follow-up traps
Why does a POST return 403 in a test? Missing CSRF token.
401 vs 403? 401 is unauthenticated, 403 authenticated but not authorized.
#spring-security#mockmvc
Q38
What is Testcontainers and why use it?
intermediate
Testcontainers starts disposable Docker containers (PostgreSQL, Kafka, Redis) from tests, so integration tests run against the real engine rather than a fake like H2.
Spring Boot 3.1+ @ServiceConnection wires properties automatically; before that use @DynamicPropertySource.
A static @Container is shared by all tests in the class; instance field means one per test.
Needs Docker in CI; Ryuk container cleans up leaked containers.
⚠ Follow-up traps
Do containers restart per test method? Only if the field is non-static.
How to speed up many test classes? Singleton container pattern or withReuse(true) (local dev only).
#testcontainers#docker#integration-test
Q39
How do you manage database state between integration tests?
intermediate
Options: roll back each test's transaction (@Transactional), truncate tables in @BeforeEach, recreate a Flyway schema, or reuse a template database. Pick based on whether the code under test commits.
@Transactional tests hide bugs involving real commits, lazy loading outside a session and REQUIRES_NEW.
A test-side @Transactional does not roll back work done on another thread (e.g. RANDOM_PORT server).
Use @Sql scripts for fixtures; avoid dependence on pre-seeded shared data.
⚠ Follow-up traps
Does @Transactional on a RANDOM_PORT test roll back the HTTP call's changes? No, the server runs in another thread and transaction.
Is @DirtiesContext a data cleanup tool? No, it discards the context at high cost.
#integration-test#database#test-isolation
Q40
What is contract testing and how does Spring Cloud Contract or Pact work?
advanced
Contract testing verifies that a consumer's expectations of a provider's API and the provider's actual behaviour agree, without deploying both together.
Consumer-driven (Pact): consumers publish expectations to a broker; the provider verifies them in CI. can-i-deploy gates releases.
Spring Cloud Contract (provider-driven): contracts in Groovy/YAML generate provider tests and consumer stubs (WireMock).
Catches breaking changes (renamed field, status code) cheaply compared to E2E.
Does not test business logic of either side.
⚠ Follow-up traps
Is contract testing a replacement for provider unit tests? No, it checks the interface only.
Who owns the contract in Pact? The consumer defines, the provider verifies.
#contract-testing#pact#spring-cloud-contract
Q41
Why do flaky tests happen and how do you fix them?
intermediate
A flaky test passes and fails with no code change. Typical causes: timing and Thread.sleep, shared state or ordering, real time/randomness, concurrency races, external services, and resource limits.
Fix the cause: inject Clock, use Awaitility for async, isolate data, seed randomness, use fakes for remote services.
Quarantine a flaky test quickly (tag it, track it) rather than blindly rerunning.
Retries (Surefire rerunFailingTestsCount) mask, not solve.
Detect via repeated runs, random ordering and CI trend dashboards.
⚠ Follow-up traps
Is Thread.sleep(1000) an acceptable wait? No; it is either too slow or too short. Poll with a timeout.
Does retrying failing tests make a suite reliable? It hides the signal and makes failures acceptable.
#flaky-tests#reliability
Q42
How do you test time-dependent code?
intermediate
Inject a java.time.Clock instead of calling Instant.now() or LocalDate.now() directly; tests use Clock.fixed(...) or a mutable test clock.
Clock clock = Clock.fixed(Instant.parse("2026-01-01T00:00:00Z"), ZoneOffset.UTC);var service = new TokenService(clock);assertTrue(service.issue().expiresAt().isAfter(clock.instant()));
LocalDate.now(clock) overloads exist on all java.time types.
Mocking Instant.now() statically is a last resort.
For scheduled work, extract logic from @Scheduled methods and call it directly.
⚠ Follow-up traps
Which zone should a fixed clock use? Explicit UTC to avoid developer-machine dependence.
Does Clock.fixed ever advance? No; create a custom Clock subclass to simulate passing time.
#time#clock
Q43
How do you test asynchronous and concurrent code?
advanced
Make the code deterministic where possible (inject an Executor that runs inline), and otherwise coordinate with latches/futures and poll with Awaitility, never with fixed sleeps.
Use CountDownLatch/CyclicBarrier to maximize contention in race tests.
Run the test many times (@RepeatedTest) to improve odds of exposing races; passing proves little.
Tools: jcstress for memory-model tests, Lincheck, ExecutorService shutdown in @AfterEach.
Exceptions on other threads do not fail the test automatically; collect and rethrow.
⚠ Follow-up traps
If an assertion fails in a worker thread, does JUnit fail? No, you must propagate it to the test thread.
Does one green run prove thread safety? No, absence of failure is not evidence.
#concurrency#awaitility#async
Q44
How do you test logging, exceptions and void methods?
basic
For void methods assert on side effects: collaborator calls (verify), state changes, or captured output. For exceptions use assertThrows/assertThatThrownBy; for logs use a test appender (Logback ListAppender) or OutputCaptureExtension.
Do not assert log text unless logging is a requirement.
@ExtendWith(OutputCaptureExtension.class) gives a CapturedOutput parameter in Spring Boot.
doThrow on a mock tests error paths.
⚠ Follow-up traps
Can a try/catch with fail() replace assertThrows? It works but is verbose and swallows subtype semantics.
Should tests check log messages? Rarely; they are brittle.
#void#exceptions#logging
Q45
Code coverage versus test quality: what does coverage tell you?
intermediate
Coverage (line, branch, instruction via JaCoCo) tells you which code was executed, not whether behaviour was verified. High coverage with no assertions is worthless; low coverage reliably signals untested areas.
Branch coverage is stronger than line coverage.
Goodhart's law: a mandated percentage encourages assertion-free tests.
Use thresholds as a floor on changed code (diff coverage), not a goal.
Exclude generated code and trivial DTOs sensibly.
⚠ Follow-up traps
Is 100% coverage bug-free? No; code can run without asserting, and missing requirements are invisible.
JaCoCo with Mockito inline or Lombok? Lombok needs lombok.addLombokGeneratedAnnotation=true so generated code is excluded.
#coverage#jacoco#quality
Q46
What is mutation testing and how does PIT work?
advanced
Mutation testing injects small defects (mutants) such as flipping > to >= or removing a call, and re-runs tests. A mutant that no test fails on has "survived", revealing a weak assertion. PIT (Pitest) is the main JVM tool.
Mutation score = killed / total mutants, a better quality proxy than coverage.
Slow, so run on changed classes or nightly (withHistory, scmMutationCoverage).
Equivalent mutants (behaviour unchanged) cause false survivors.
Plugin for JUnit 5: pitest-junit5-plugin.
⚠ Follow-up traps
Does a covered line guarantee mutants die? No, covered lines often survive because nothing asserts the effect.
Why not run PIT on every commit? It multiplies test run time by the number of mutants.
#mutation-testing#pitest
Q47
How do you test with a real HTTP dependency: WireMock vs MockRestServiceServer?
intermediate
WireMock runs a real HTTP server with stubbed endpoints, so the full client stack (serialization, timeouts, retries, TLS) is exercised. MockRestServiceServer replaces the RestTemplate request factory in-process and is faster but skips the network layer.
WireMock supports fault injection (delays, connection reset) to test resilience.
@RegisterExtension WireMockExtension with a dynamic port; point base URL at it.
Verify requests with verify(postRequestedFor(urlEqualTo("/pay"))).
⚠ Follow-up traps
Does MockRestServiceServer test connection timeouts? No, no real connection exists.
Is a hand-written stub a contract? No, it can drift from the real API; pair with contract tests.
#wiremock#http-stubbing
Q48
What is the difference between @Mock-based unit tests and Spring slice tests for a service layer?
intermediate
Service classes with constructor injection should be tested as plain Java with Mockito and no Spring; slices are for verifying framework behaviours such as mapping, validation, transactions and serialization.
Constructor injection makes new OrderService(repo, clock) possible without reflection.
@Transactional on a service only works through the Spring proxy, so a plain unit test cannot verify transactional behaviour.
Self-invocation and private methods also bypass proxies.
⚠ Follow-up traps
Can a plain Mockito test prove @Transactional rolls back? No, needs an integration test with a real transaction manager.
Why prefer constructor over field injection for testing? No reflection or container is required.
#unit-test#service-layer#spring
Q49
How does JUnit 5 handle test execution order and how do Surefire and Failsafe differ?
intermediate
JUnit does not guarantee method order unless you set @TestMethodOrder. Surefire runs unit tests (*Test, *Tests, Test*) in the test phase; Failsafe runs *IT/*ITCase in integration-test and checks results in verify, so teardown still happens when tests fail.
Run only integration tests: mvn verify -DskipUTs style flags or profiles.
Gradle uses separate source sets and tasks for integration tests.
Tests that rely on order should be merged into one scenario test.
⚠ Follow-up traps
Why use mvn verify rather than mvn integration-test? Failsafe fails the build in verify, after post-integration-test cleanup.
Is a default order stable between runs? Deterministic but not obvious; do not depend on it.
#maven#surefire#failsafe#ordering
Q50
How do you test code that uses randomness, UUIDs or external IDs?
basic
Inject the source: a Random with a fixed seed, a Supplier<UUID>, or an IdGenerator interface. Assert on properties (format, uniqueness) when exact values are not needed.
new Random(42) is deterministic for a given JDK algorithm.
For hashing and ordering tests, use fixed fixtures.
Log the seed when using randomized or property-based tests (jqwik) so failures are reproducible.
⚠ Follow-up traps
Is Random(seed) stable across JDK versions? The java.util.Random algorithm is specified and stable; ThreadLocalRandom cannot be seeded.
When is property-based testing useful? For invariants over wide input spaces, such as parse/format round trips.
#determinism#random
Scenarios
Q51
A test stubs `when(repo.findById(1L)).thenReturn(Optional.of(u))` but the code calls `findById(2L)`. What happens under strict stubs?
intermediate
With MockitoExtension (strict stubs) the call findById(2L) raises PotentialStubbingProblem because the mock was stubbed for different arguments. Without strictness it silently returns Optional.empty() (Mockito's default for Optional) and the test fails later with a confusing message.
The strict message lists the stubbed and actual arguments, pointing straight at the mismatch.
If the stub is never used, UnnecessaryStubbingException is reported at the end.
⚠ Follow-up traps
What does an unstubbed Optional return type give?Optional.empty(), not null.
How to allow it deliberately?lenient().when(...) or Strictness.LENIENT.
#mockito#strict-stubs#scenario
Q52
What does this test print or fail with?
intermediate
List<String> real = new ArrayList<>();List<String> spy = spy(real);spy.add("a");System.out.println(real.size() + " " + spy.size());
Output: 0 1. spy() copies the instance, so real is untouched and only the spy's own state changes.
To observe state on the original, assert on the spy itself.
when(spy.get(0)).thenReturn("x") on an empty list throws IndexOutOfBoundsException because it calls the real get(0); use doReturn("x").when(spy).get(0).
⚠ Follow-up traps
Does spy modify real? No, it works on a copy.
Can you spy on a final class? Yes with the inline mock maker (default in Mockito 5).
#mockito#spy#scenario
Q53
A `@BeforeEach` sets up stubs used by only some tests and the suite fails with UnnecessaryStubbingException. How do you fix it?
basic
Move each stub into the tests that need it, or mark shared ones lenient(). Moving is better since it keeps tests self-explanatory; lenient() is acceptable for truly common defaults.
Do not disable strictness globally; unused stubs are real dead code and often hide a bug (the code path was never executed).
Alternative: build a helper method stubHappyPath() called explicitly.
⚠ Follow-up traps
Is the failure reported on the test or the class? Reported at the end of the test method in which the stub was unused.
Does @MockitoSettings(strictness = LENIENT) fix it? Yes, but it hides future dead stubs.
#mockito#strict-stubs#setup#scenario
Q54
A controller test returns 403 for a POST even though the user has the right role. Why?
intermediate
CSRF protection is on by default in Spring Security, and @WebMvcTest includes the security filter chain. Add .with(csrf()) to the request or disable CSRF in the security config for stateless APIs.
If the role is wrong you would get 403 too; with no authentication at all, 401 (or a redirect to login for form login).
⚠ Follow-up traps
GET also fails with 403? Then it is authorization (role/authorities, ROLE_ prefix), not CSRF.
Does @WithMockUser(roles = "ADMIN") grant ROLE_ADMIN? Yes, the prefix is added.
#spring-security#csrf#webmvctest#scenario
Q55
`@WebMvcTest` fails with "No qualifying bean of type UserService". What do you do?
basic
The slice does not component-scan @Service beans. Declare @MockBean/@MockitoBean UserService service; or @Import a real implementation and its dependencies.
Do not switch to @SpringBootTest just to silence this; you lose the speed of the slice.
If the controller needs many beans, that may indicate it is doing too much.
⚠ Follow-up traps
Why are filters and @ControllerAdvice still loaded? They are part of the web slice by type filter.
Does @MockBean return null for unstubbed calls? Yes, defaults such as null, empty collections.
#webmvctest#mockbean#scenario
Q56
A repository test with H2 passes locally but the same query fails in production PostgreSQL. What is the fix?
intermediate
H2 differs in SQL dialect, JSON/array types, locking, case handling and constraint behaviour. Run @DataJpaTest against a Testcontainers PostgreSQL with @AutoConfigureTestDatabase(replace = Replace.NONE), using the same Flyway migrations.
H2 MODE=PostgreSQL narrows gaps but does not remove them.
⚠ Follow-up traps
Does @DataJpaTest use your real DataSource by default? No, it swaps in an embedded DB.
Does ddl-auto=create-drop validate migrations? No; use Flyway/Liquibase in tests too.
#testcontainers#h2#datajpatest#scenario
Q57
A `@Transactional` test passes, but production throws `LazyInitializationException`. Why?
advanced
The test method's transaction keeps the persistence context open, so lazy associations load at any point. In production the service transaction ends and the entity is detached before the view/serializer touches the lazy field.
Fix the code (fetch join, @EntityGraph, DTO projection) and make the test expose it: remove test @Transactional, or call entityManager.flush(); entityManager.clear(); before asserting, or run via the real service boundary.
Same effect hides missing flushes and constraint violations until commit.
⚠ Follow-up traps
Does @Transactional on a test method roll back by default? Yes, after the test.
Does clear() alone expose the problem? Only if the access happens outside an open session in the test.
#transactional#jpa#lazy-loading#scenario
Q58
An integration test with `@SpringBootTest(webEnvironment = RANDOM_PORT)` and `@Transactional` leaves data behind. Why?
advanced
The HTTP request is handled by a server thread with its own transaction that commits; the test-managed transaction on the test thread does not cover it, so rollback never undoes those writes.
Clean up explicitly: @Sql cleanup script, truncating in @AfterEach, or Testcontainers per class.
With MOCK environment and MockMvc the request runs on the test thread and the rollback does work.
⚠ Follow-up traps
Does MockMvc share the test transaction? Yes, same thread.
Is @DirtiesContext an appropriate cleanup? No, it is expensive and unrelated to data.
Test suite time doubled after adding `@MockBean` to a few test classes. Why and how do you fix it?
advanced
Spring caches application contexts by configuration key. Each distinct combination of @MockBeans, @TestPropertySource, active profiles or @DirtiesContext produces a new context, which must be started again.
Consolidate mocks in a shared abstract base class or a @TestConfiguration so many classes use one key.
Prefer slices (@WebMvcTest) and plain unit tests to reduce @SpringBootTest count.
Check the log line "Started ... in" count, and enable context cache logging (logging.level.org.springframework.test.context.cache=DEBUG).
⚠ Follow-up traps
Does @DirtiesContext help speed? No, it forces a restart.
Is the cache shared across JVM forks? No, each forked JVM has its own cache.
#springboottest#context-cache#mockbean#scenario
Q60
Test uses `Thread.sleep(2000)` to wait for an async listener and is flaky in CI. Fix it.
intermediate
Replace the sleep with polling for the observable outcome using Awaitility; it returns as soon as the condition holds and fails only after a timeout.
A fixed sleep is simultaneously slow on fast machines and too short on slow ones.
If possible make the executor injectable and synchronous in tests.
⚠ Follow-up traps
What is Awaitility's default timeout? 10 seconds.
Can the lambda's exceptions end the wait early? Not with untilAsserted; assertion errors are retried until timeout.
#flaky-tests#awaitility#scenario
Q61
A test calls `LocalDate.now()` and fails only on the last day of a month. How do you fix the design?
basic
The code depends on the real current date. Inject Clock into the class, call LocalDate.now(clock), and construct the test with Clock.fixed(...) for the boundary case you want, such as 31 January.
Add tests for edge dates: month end, leap day 29 Feb, DST change, year boundary.
Avoid ZoneId.systemDefault() in code and tests; use an explicit zone.
⚠ Follow-up traps
Why also fail on machines in other time zones?LocalDate.now() uses the system zone, so "today" differs.
Is mockStatic(LocalDate.class) a good alternative? Works, but is intrusive and thread-local; prefer a Clock.
#time#clock#flaky-tests#scenario
Q62
What is the output of this parameterized test?
basic
@ParameterizedTest@CsvSource({"1,2,3", "2,2,4", "5,5,11"})void adds(int a, int b, int sum) { assertEquals(sum, a + b); }
The test runs three invocations; the first two pass and the third fails (5 + 5 = 10, not 11). The report shows 2 passed, 1 failed with the failing row's arguments; the other rows are not affected.
Display names default to [3] 5, 5, 11.
@CsvSource converts strings to int implicitly.
⚠ Follow-up traps
Does the build stop at the first failing row? No, all rows execute.
How to pass null in @CsvSource? Leave a field empty or use nullValues = "NULL".
#junit5#parameterized#scenario
Q63
A static `@MockedStatic` was created but not closed; other tests now behave strangely. Explain.
advanced
mockStatic registers a thread-local mock on the creating thread. If it is not closed, the static stays mocked for later tests on that thread, and the next mockStatic of the same class throws "static mocking is already registered in the current thread".
try (MockedStatic<Files> f = mockStatic(Files.class)) { f.when(() -> Files.exists(any())).thenReturn(true); // code under test} // closed here, real behaviour restored
Use try-with-resources or @AfterEach close; never create in @BeforeAll shared across parallel tests.
Mocking JDK classes broadly (Files, System) can break the test framework itself.
⚠ Follow-up traps
Does parallel execution leak static mocks across threads? No, they are thread-scoped, but sequential tests on the same thread do see leaks.
What is the cleaner design? Wrap the static call behind an injectable interface.
#mockito#mockstatic#scenario
Q64
You must test code that calls `new HttpClient()` internally. How?
intermediate
Best: refactor to inject the client or a factory. Fallback: mockConstruction(HttpClient.class, (mock, ctx) -> when(mock.send(...)).thenReturn(resp)) intercepts constructors invoked within the scope of the try block.
mockConstruction also works with the inline mock maker only.
Hard-to-test code is a design signal: new-ing collaborators inside methods couples the class to their implementation.
For HTTP specifically, point the real client at WireMock.
⚠ Follow-up traps
Does mockConstruction affect other threads? It is thread-local like mockStatic.
Does it need a close? Yes, it returns a MockedConstruction to close.
#mockito#mockconstruction#refactoring#scenario
Q65
A test verifies `verify(emailService).send(any())` but the email is sent with the wrong recipient. How do you catch it?
basic
any() accepts everything, so the verification is vacuous. Capture the argument and assert on fields, or use argThat with a precise predicate.
var cap = ArgumentCaptor.forClass(Email.class);verify(emailService).send(cap.capture());assertThat(cap.getValue().to()).isEqualTo("ann@x.com");
If Email implements equals, verify(emailService).send(expectedEmail) is simplest.
⚠ Follow-up traps
Which call do captors return with multiple invocations?getValue() returns the last; getAllValues() returns all.
Does verify ignore the order of calls? Yes, unless InOrder is used.
#mockito#argumentcaptor#verify#scenario
Q66
A test passes alone but fails when the full suite runs. What are the usual causes and how do you investigate?
intermediate
Shared mutable state leaks between tests: static fields or singletons, cached Spring context mutated by a test, leftover database rows, system properties, default Locale/TimeZone, files in a shared temp dir, or unclosed static mocks.
Reproduce with the failing pair and random order (junit.jupiter.testmethod.order.default=...Random, Surefire runOrder=random).
Bisect by running subsets; print state in @AfterEach.
Fix by resetting in @BeforeEach/@AfterEach, or by removing the global state.
⚠ Follow-up traps
Does @DirtiesContext cure order dependence? It hides it for context state at a high cost.
Do Mockito mocks leak across tests? Not with the extension; each test gets fresh mocks.
#flaky-tests#test-isolation#scenario
Q67
Two `@Test` methods modify a `static` list and one fails in parallel mode. What do you do?
intermediate
Parallel execution exposes the shared mutable static. Remove the sharing (instance field, new per test), or protect with @ResourceLock("LIST"), or force @Execution(SAME_THREAD) for that class.
Default instance-per-method lifecycle already gives each test a fresh instance, so use instance fields.
Fixtures on shared DB rows or ports need the same treatment: unique data per test.
⚠ Follow-up traps
Does @ResourceLock serialize all tests? Only those declaring the same resource key (read/write modes exist).
Does Surefire parallel equal JUnit parallel? No, they are separate mechanisms.
Only checked exceptions declared by the method can be thrown by thenThrow; otherwise Mockito reports "Checked exception is invalid for this method".
⚠ Follow-up traps
Can thenThrow(new IOException()) stub a method without throws? No, Mockito rejects it.
Is verifying the logger call needed? Usually not.
#exceptions#mockito#scenario
Q70
How would you test a method that returns a `CompletableFuture`?
intermediate
Join with a timeout and assert the result, or inject a direct/inline executor so the task completes on the calling thread.
var result = service.fetchAsync(1).get(2, TimeUnit.SECONDS);assertEquals("ok", result);
Test failure paths with assertThrows(ExecutionException.class, ...) and check getCause().
Always use a timeout; plain get() can hang the build.
Use Runnable::run as Executor for deterministic tests; mock futures with CompletableFuture.completedFuture/failedFuture.
⚠ Follow-up traps
What does join() throw on failure?CompletionException, whereas get() throws ExecutionException.
Do tests with supplyAsync and the common pool run deterministically? No, they depend on scheduling.
#concurrency#completablefuture#scenario
Q71
Race condition test: how do you try to expose a thread-safety bug in a counter?
advanced
Start N threads behind a latch so they run together, repeat many increments, then assert the final total.
var counter = new UnsafeCounter();var start = new CountDownLatch(1);var pool = Executors.newFixedThreadPool(8);for (int i = 0; i < 8; i++) pool.submit(() -> { start.await(); for (int j = 0; j < 10_000; j++) counter.inc(); return null; });start.countDown(); pool.shutdown(); pool.awaitTermination(5, TimeUnit.SECONDS);assertEquals(80_000, counter.get());
With a non-atomic count++ the assertion typically fails with a lower number; a correct AtomicInteger always passes.
A pass does not prove safety; use jcstress or review the memory model.
Shut down pools in a finally/@AfterEach to avoid leaking threads.
⚠ Follow-up traps
Why use a start latch? To maximize contention instead of threads running one after another.
Does volatile make count++ atomic? No.
#concurrency#race-condition#scenario
Q72
A scheduled job `@Scheduled(cron=...)` needs tests. What is your approach?
intermediate
Extract the job body into a method (processDueInvoices(Instant now)), unit test that with a fixed Clock, and trust the framework for triggering. Add one small integration test verifying that the cron expression is valid (CronExpression.parse) and the bean is registered.
Avoid waiting for real cron times in tests.
Awaitility with a fixedDelay of a few ms property override is an option for wiring tests.
Distributed locks (ShedLock) are tested with an integration test and a real DB.
⚠ Follow-up traps
Should tests rely on @EnableScheduling? Disable it in most tests to avoid background runs.
How to check the cron string cheaply?CronExpression.parse("0 0 * * * *") throws for invalid input.
#scheduling#time#scenario
Q73
A test creates users via a 12-argument constructor and 40 tests break when a field is added. How do you fix it?
basic
Introduce a test data builder (or Object Mother returning a builder) with valid defaults, so constructor changes affect one place, and each test states only the fields relevant to its scenario.
The test intent improves: aUser().withAge(17).build() says what matters.
Keep builders in src/test, not production.
Combine with Instancio or EasyRandom for large graphs if exact values are irrelevant.
⚠ Follow-up traps
Should the builder's defaults be valid objects? Yes, otherwise each test repairs them.
Why not reuse production builders? They expand production API and may skip validation defaults.
#test-data-builder#maintainability#scenario
Q74
How do you test a REST client with timeout and retry behaviour?
advanced
Use WireMock fault injection: fixed delay longer than the client timeout, Fault.CONNECTION_RESET_BY_PEER, or scenario states that return 503 twice then 200.
Assert the final result is 0.9 and verify(2, getRequestedFor(urlEqualTo("/rate"))).
Keep timeouts small in test config to keep suites quick.
Check that non-idempotent requests (POST) are not retried blindly.
⚠ Follow-up traps
Can MockRestServiceServer test read timeouts? No, there is no real socket.
Does a 4xx deserve retries? Generally no.
#wiremock#resilience#scenario
Q75
A Testcontainers-based suite is slow because every test class starts a new PostgreSQL. What do you do?
advanced
Share one container across classes: the singleton container pattern (static field in an abstract base class started once, closed by Ryuk at JVM exit), or Spring Boot's @ServiceConnection on a bean in a shared @TestConfiguration.
Reset data between tests rather than restarting the DB (truncate or per-test schema/template DB).
withReuse(true) plus testcontainers.reuse.enable=true keeps containers across runs locally; do not use in CI.
Use a lighter image tag and start dependent containers in parallel with Startables.deepStart.
⚠ Follow-up traps
Who stops a singleton container? The Ryuk resource reaper when the JVM exits.
Does @Container on a static field restart per test method? No, once per class.
#testcontainers#performance#scenario
Q76
A `@SpringBootTest` fails in CI with "Could not find a valid Docker environment" but passes locally. Why?
basic
CI runner has no reachable Docker daemon. Provide Docker (privileged runner, DinD sidecar, or a runner with the socket) or the Testcontainers Cloud agent, and make sure DOCKER_HOST is set when needed.
Mark such tests with @Testcontainers(disabledWithoutDocker = true) for optional environments, but do not let CI silently skip them.
Pull-rate limits on public registries: use a mirror and set hub.image.name.prefix.
⚠ Follow-up traps
Should integration tests be skipped silently when Docker is missing? In CI no, because it hides regressions.
Where do you configure the registry prefix?testcontainers.properties.
#testcontainers#ci#scenario
Q77
The `@DataJpaTest` shows the repository `save` works, but the unique constraint violation is never thrown. Why?
advanced
Hibernate delays SQL until flush. The test transaction rolls back and never flushes, so the constraint is not checked. Call repo.saveAndFlush(entity) or entityManager.flush() inside assertThrows(DataIntegrityViolationException.class, ...).
With GenerationType.IDENTITY the insert is executed immediately, so the behaviour differs by id strategy.
Clear the persistence context to verify reload from the DB rather than the first-level cache.
⚠ Follow-up traps
Why does it throw with IDENTITY but not SEQUENCE? IDENTITY needs an immediate insert to get the id; SEQUENCE defers it.
Exception type thrown by Spring Data?DataIntegrityViolationException (translated), not raw ConstraintViolationException.
#datajpatest#flush#jpa#scenario
Q78
What happens in this test, and what is the improvement?
intermediate
assertThat(list.contains("a"));
Nothing is asserted: assertThat(boolean) returns an assert object and no terminal method is called, so the test always passes. Use assertThat(list).contains("a") for a descriptive failure message.
Static analysis (Error Prone, SonarQube) can flag dangling assertThat.
Also avoid assertTrue(list.contains("a")) since the failure message is just "expected true".
⚠ Follow-up traps
Does assertThat(x).isEqualTo(y) fail with the same message as assertTrue(x.equals(y))? No, AssertJ prints both values.
How to enforce terminal calls? Use Sonar rule S2970 or Error Prone checks.
#assertj#assertions#scenario
Q79
How would you test an `equals`/`hashCode` implementation?
intermediate
Use EqualsVerifier, which checks reflexivity, symmetry, transitivity, consistency, null handling and the hashCode contract, including subclass and mutable-field pitfalls.
EqualsVerifier.forClass(Money.class).verify();
Hand-written asserts miss cases such as symmetry with subclasses or null fields.
JPA entities need .suppress(Warning.SURROGATE_KEY) or specific config depending on the strategy.
⚠ Follow-up traps
Does a passing EqualsVerifier mean equals matches business meaning? No, only the contract.
Do Lombok @EqualsAndHashCode classes need this? Generated code is correct but field selection may not be.
#equals#hashcode#equalsverifier#scenario
Q80
A team enforces 90% line coverage and tests are now full of assertion-free calls. How do you respond?
intermediate
Coverage as a target gets gamed. Switch to meaningful signals: branch coverage on changed code (diff coverage), mutation score via PIT on critical modules, and code review of test intent.
Run PIT in CI nightly; surviving mutants list exactly the weak assertions.
Remove coverage gates on trivial code; keep a floor, not a target.
Add assertion-less test detection via Sonar.
⚠ Follow-up traps
Does PIT replace JaCoCo? No; PIT only mutates covered lines, so coverage is still the first filter.
What is an equivalent mutant? A change that does not alter behaviour, so no test can kill it.
#coverage#mutation-testing#scenario
Q81
PIT reports a surviving mutant at `if (age >= 18)` changed to `age > 18`. What does it mean?
advanced
No test exercises the boundary value 18, so the test suite cannot distinguish >= from >. Add a test (or parameterized row) with age exactly 18 expecting adult.
Boundary-value analysis is the general fix for "conditional boundary" mutants.
⚠ Follow-up traps
Does 100% branch coverage guarantee a kill? No, both branches may execute without testing the boundary.
Is a survivor always a test problem? Sometimes it is an equivalent mutant or dead code.
#pitest#boundary#scenario
Q82
Mocking `UUID.randomUUID()` is hard in a legacy class. What is the cleanest refactor?
basic
Inject a Supplier<UUID> (or IdGenerator) via the constructor; production passes UUID::randomUUID, tests pass () -> FIXED.
class OrderFactory { private final Supplier<UUID> ids; OrderFactory(Supplier<UUID> ids) { this.ids = ids; } Order create() { return new Order(ids.get()); }}
Same technique for Clock, Random, System.getenv.
It avoids the thread-local pitfalls of mockStatic.
⚠ Follow-up traps
Is mockStatic(UUID.class) wrong? It works, but it is a workaround for missing seams.
Do you also need an interface?Supplier or a functional interface is enough.
#determinism#refactoring#scenario
Q83
A test passes with `@MockBean` on a service but fails when replaced by `@Mock` and `@InjectMocks`. What differs?
intermediate
@InjectMocks uses constructor, then setter/field injection; if the target has two constructor args and only one @Mock exists, the missing one is null. Field injection by type is ambiguous with two mocks of the same type, so Mockito uses the field name.
Spring wiring (@Value, @Autowired by qualifier, proxies, @PostConstruct) does not run, so values stay default.
Create the object explicitly: new Service(repo, clock); then no injection surprises exist.
⚠ Follow-up traps
Does @InjectMocks call @PostConstruct? No.
Do @Value fields get populated with @InjectMocks? No, use ReflectionTestUtils.setField or constructor args.
#injectmocks#spring-boot#scenario
Q84
How do you test a `@Value`-bound configuration class or `@ConfigurationProperties`?
intermediate
Use ApplicationContextRunner (no full context) or @SpringBootTest(classes = ..., properties = ...); or @EnableConfigurationProperties in a minimal slice.
new ApplicationContextRunner() .withUserConfiguration(AppConfig.class) .withPropertyValues("app.timeout=5s") .run(ctx -> assertThat(ctx.getBean(AppProps.class).timeout()) .isEqualTo(Duration.ofSeconds(5)));
Test validation failures: run(ctx -> assertThat(ctx).hasFailed()).
Same runner tests conditional auto-configuration.
⚠ Follow-up traps
Does ApplicationContextRunner start the web server? No.
Does @ConfigurationProperties validation need @Validated? Yes, with a Bean Validation provider.
#spring-boot#configuration-properties#scenario
Q85
How do you test a Kafka consumer in a Spring Boot app?
advanced
Unit test the handler logic with plain objects; for wiring, run a Kafka Testcontainer (or @EmbeddedKafka), publish a record with KafkaTemplate, and use Awaitility to assert the effect, since consumption is asynchronous.
Use unique topic and group names per test (or a fresh container) to avoid cross-test offsets.
Set auto.offset.reset=earliest for the test consumer so it sees the record.
Test idempotency by sending the same record twice and the dead-letter path with a poison message.
⚠ Follow-up traps
Why does the consumer sometimes miss the message? It subscribed after the send with latest offset reset.
Is Thread.sleep ok for waiting? No; poll with a timeout.
#testcontainers#kafka#integration-test#scenario
Q86
When would you choose contract tests over E2E tests between two microservices?
intermediate
When the main risk is interface drift (field renamed, status code changed) and the services are owned by different teams. Contract tests run independently in each pipeline in seconds, while E2E needs both deployed and is slow and flaky.
Keep a few E2E smoke tests for critical journeys.
Pact can-i-deploy and provider verification on consumer pact changes catch breakage before release.
Contracts should assert structure and essential values with matchers, not exact data.
⚠ Follow-up traps
Should a contract assert every provider field? No, only the fields consumers use, so the provider stays free to evolve.
Is a contract test a test of business rules? No.
#contract-testing#microservices#scenario
Q87
A consumer pact fails on provider verification for a new required header. Who fixes what?
advanced
Provider verification failed because the provider no longer satisfies the consumer's expectation (or the consumer added a new one). Decide who is right: if the consumer needs the header, the provider adds support; if the provider changed incompatibly, it must stay backward compatible (add, deprecate, then remove) before verification passes.
Use provider states to set up data (given("user 1 exists")).
Pending pacts and WIP pacts stop new consumer expectations from failing the provider build unexpectedly.
⚠ Follow-up traps
What are provider states? Setup hooks that put the provider in the data state a pact expects.
What does can-i-deploy check? Whether a version's pacts are verified against the target environment's versions.
#pact#contract-testing#scenario
Q88
Controller test with `MockMvc` returns 200 but the JSON is wrong. How do you assert JSON properly?
basic
Use jsonPath or content().json(expected, strict) (JSONassert) rather than string equality, so field order and whitespace do not matter.
content().json(str) is lenient by default (extra fields allowed); pass true for strict.
MvcResult plus ObjectMapper lets you deserialize into a DTO and use AssertJ.
⚠ Follow-up traps
Does lenient JSON compare allow extra fields? Yes.
Does it check array order in lenient mode? Order is not checked in lenient mode; strict checks order.
#mockmvc#json#scenario
Q89
How do you test validation (`@Valid`) and the exception handler returning 400?
basic
Send an invalid payload through MockMvc in @WebMvcTest; MethodArgumentNotValidException is handled by the @ControllerAdvice (also in the slice) or default handler, giving status 400.
Unit-test the validator itself with Validation.buildDefaultValidatorFactory() for custom constraints.
Missing @Valid means no validation: the test would return 2xx and expose it.
⚠ Follow-up traps
Does constructor-level @NotNull on a service parameter validate? Only with @Validated on the bean (method validation) and a proxy.
Spring Boot 3 import?jakarta.validation, not javax.
#validation#webmvctest#scenario
Q90
Mock returns `null` for a `Map` return and the code NPEs. Why not?
basic
Mockito's RETURNS_DEFAULTS returns empty collections (Map, List, Set), empty Optional/Stream, 0/false for primitives, and null for other object types. So getSettings() returning Map gives an empty map, not null, but a method returning a custom Config gives null and may NPE.
Stub explicitly rather than relying on defaults; hidden defaults make tests misleading.
RETURNS_SMART_NULLS improves the failure message for such NPEs.
⚠ Follow-up traps
Does a String return default to ""? No, null.
Does int-returning method give null? No, 0.
#mockito#defaults#scenario
Q91
Verify that a method was called exactly once even though it is called inside a loop with different args. How?
intermediate
Use verify(mock, times(1)).send(eq("a")) per argument, or verify(mock, times(3)).send(anyString()) for the total, and capture values for the set check.
Plain verify(notifier).send(anyString()) fails with TooManyActualInvocations for 3 calls.
⚠ Follow-up traps
What exception does Mockito raise for a wrong count?TooManyActualInvocations or TooFewActualInvocations.
Is the order of getAllValues() the call order? Yes.
#mockito#verify#scenario
Q92
Why is `Mockito.mock(String.class)` failing or bad practice, and what do you use for value objects?
basic
String is a final JDK class and mocking it is rejected or fragile, and mocking any simple value object is pointless: use real instances ("abc", new Money(10)). Mock only collaborators with behaviour or I/O.
Rule of thumb: do not mock what you own if cheap to construct, and do not mock types you do not own (wrap them).
Over-mocking produces tests that restate the implementation.
⚠ Follow-up traps
Can you mock List? You can, but a real ArrayList is simpler and more faithful.
Is mocking an ObjectMapper a good idea? No, use a real one.
#mockito#value-objects#scenario
Q93
`@SpringBootTest` run reuses a cached context where a test altered a bean's state. What is the symptom and fix?
advanced
Later tests see leftover state (cache entries, counters, in-memory queues) because singleton beans are shared across cached contexts. Fix by resetting the state in @AfterEach (cache.clear()), using @MockBean resets, or as a last resort @DirtiesContext for that class.
@DirtiesContext forces context rebuild after the class or method, costing seconds.
Design beans so tests can reset them explicitly.
⚠ Follow-up traps
Are Mockito mocks from @MockBean reset automatically? Yes, after each test.
How do you test a method protected with `@PreAuthorize("hasRole('ADMIN')")`?
intermediate
Load a context with method security enabled (@SpringBootTest or a slice with @EnableMethodSecurity imported) and annotate the test with @WithMockUser(roles = "ADMIN") for the success case; for the other, expect AccessDeniedException.
A plain new Service() unit test will never apply @PreAuthorize because it relies on the Spring proxy.
⚠ Follow-up traps
Why does a unit test not enforce the annotation? No proxy means no interceptor.
Does @WithMockUser need a user in the DB? No; use @WithUserDetails for a real lookup.
#spring-security#method-security#scenario
Q95
A test that uses `new Random()` fails 1 in 50 runs. How do you handle it?
basic
Treat it as a real bug until proven otherwise: log the seed on failure, replay it, and fix either the code (e.g. unhandled edge value) or the test (unfounded assumption). Make tests deterministic by seeding or by passing explicit values.
Property-based frameworks (jqwik) print the failing seed and shrink inputs.
Do not simply rerun and ignore.
⚠ Follow-up traps
Is a flaky test always a test bug? No, it often reveals a race or edge case in production code.
Can ThreadLocalRandom be seeded? No.
#flaky-tests#random#scenario
Q96
How do you structure test code to avoid duplication without hiding intent?
intermediate
Extract setup into builders and small helper methods with intention-revealing names, keep arrange/act/assert visible in each test, and prefer a bit of duplication (DAMP) over clever shared fixtures (DRY) so a failure is readable alone.
Use @Nested to group shared context.
Avoid inheritance chains of test base classes with hidden state.
What is DAMP? Descriptive And Meaningful Phrases: favour readability in tests over deduplication.
Is a giant shared @BeforeEach fine? No, tests then depend on setup they do not mention.
#test-design#maintainability#scenario
Q97
A slow `@SpringBootTest` suite takes 20 minutes. Outline a plan to speed it up.
advanced
Measure first, then cut context starts and external waits.
Count distinct contexts; consolidate @MockBean sets and profiles; use slices and plain unit tests where a full context is unneeded.
Share one Testcontainers instance; truncate data rather than restart.
Run Surefire/Failsafe with parallel forks and JUnit parallel where tests are isolated.
Lazy initialization (spring.main.lazy-initialization=true) in tests.
Replace sleeps with Awaitility; run only affected modules in PRs and the full suite on merge.
⚠ Follow-up traps
Does lazy init change behaviour? Beans start on first use, so wiring errors may show up later.
Is more parallelism always faster? No, shared DB or CPU contention can slow it.
#test-performance#springboottest#scenario
Q98
A test mocks `repository.save(entity)` to return the entity but the code under test uses the returned instance's id. What goes wrong?
intermediate
An unstubbed save returns null, causing an NPE. Stub with an Answer that mimics JPA by assigning the id and returning the argument.
when(repo.save(any(Order.class))).thenAnswer(inv -> { Order o = inv.getArgument(0); o.setId(42L); return o;});
Alternatively use a fake in-memory repository for several tests.
Do not blindly return the same argument unless the code does not depend on generated fields.
⚠ Follow-up traps
Does real save always return the same instance? No; merge for detached entities returns a different managed instance.
Should you use returnsFirstArg()? AdditionalAnswers returnsFirstArg() is a concise equivalent.
#mockito#jpa#answer#scenario
Q99
Test calls a method that logs and swallows exceptions; how do you make the failure visible?
intermediate
Assert on the observable consequence (returned fallback, metric, retry call) and optionally capture logs with a Logback ListAppender or OutputCaptureExtension. Swallowed exceptions are a design smell: consider returning a result type or rethrowing.
var appender = new ListAppender<ILoggingEvent>(); appender.start();((Logger) LoggerFactory.getLogger(Job.class)).addAppender(appender);job.run();assertThat(appender.list).extracting(ILoggingEvent::getLevel).contains(Level.ERROR);
⚠ Follow-up traps
Should the appender be removed? Yes, detach it in @AfterEach to avoid leaks.
Is asserting the exact message wise? No; assert level and key content.
#exceptions#logging#scenario
Q100
How would you test file I/O code safely?
basic
Use @TempDir Path dir so each test writes into an isolated, auto-deleted directory, and accept a Path or FileSystem in the code under test.
Avoid hard-coded paths and /tmp names that collide in parallel runs.
In-memory file systems (Jimfs) can simulate Windows/Unix differences.
⚠ Follow-up traps
Is the temp directory deleted if the test fails? Yes, by default (cleanup mode can keep it).
Does it work as a static field? Yes, one directory shared by all tests in the class.
#tempdir#io#scenario
Q101
What would you test at which level for a new "place order" feature?
advanced
Distribute by risk and cost.
Unit: pricing, discount and validation rules in the domain (plain JUnit, no mocks where possible).
Service unit tests with Mockito for orchestration branches (payment failure, out of stock).
@WebMvcTest: request validation, status codes, JSON mapping, security rules.
@DataJpaTest with Testcontainers: repository queries and constraints.
One or two @SpringBootTest integration tests of the happy path plus a rollback path with real transactions.
Contract test for the payment provider API; one E2E smoke test.
⚠ Follow-up traps
Should every layer repeat the same assertion? No, each level tests what only it can prove.
Where do you test transaction rollback on payment failure? In an integration test with a real transaction manager.
#test-pyramid#strategy#scenario
Q102
How would you introduce tests into a legacy class with static calls and `new` everywhere?
advanced
Start with characterization tests that pin current behaviour (often at a higher level), then create seams with small safe refactorings (extract method, introduce constructor-injected interface, wrap statics) and move to finer unit tests.
Use mockStatic/mockConstruction temporarily to get a test in place, then remove them with the refactor.
Test asserts `assertEquals(0.3, 0.1 + 0.2)`. What happens and what is the fix?
basic
It fails: 0.1 + 0.2 is 0.30000000000000004. Use a delta overload assertEquals(0.3, 0.1 + 0.2, 1e-9), or AssertJ isCloseTo(0.3, within(1e-9)); for money use BigDecimal with compareTo or isEqualByComparingTo.
assertEquals(BigDecimal("2.0"), BigDecimal("2.00")) also fails since scale differs under equals.
⚠ Follow-up traps
Does assertEquals(double, double) without delta compile in JUnit 5? Yes, it compares exactly via Double.doubleToLongBits-style equality.
BigDecimal.equals vs compareTo?equals compares scale, compareTo does not.
#assertions#floating-point#scenario
Q104
Why might `assertThrows` pass while the test is wrong, and how do you tighten it?
intermediate
assertThrows(RuntimeException.class, ...) passes for any runtime exception, including an unrelated NullPointerException from a bug in the test setup. Assert the specific type and message or cause, and keep the lambda to the single statement under test.
var ex = assertThrowsExactly(InsufficientFundsException.class, () -> account.withdraw(500));assertEquals(100, ex.getShortfall());
Put arrange code outside the lambda so setup errors fail clearly.
⚠ Follow-up traps
Does assertThrows fail if no exception is thrown? Yes, with AssertionFailedError.
Does it match subtypes? Yes; assertThrowsExactly does not.
#assertions#exceptions#scenario
Q105
How do you test a REST endpoint end-to-end with an authenticated JWT in `@SpringBootTest`?
advanced
Either generate a signed token with a test key and send it as a Bearer header via TestRestTemplate/WebTestClient, or in MockMvc use jwt() post-processor to skip signature validation while testing authorities.
Use a mock JWK/issuer (WireMock or Keycloak container) for full-flow tests of decoding and signature.
Test expired and wrong-audience tokens produce 401.
⚠ Follow-up traps
Does jwt() validate signatures? No, it injects an authenticated JwtAuthenticationToken directly.
401 or 403 for an expired token? 401.
#springboottest#jwt#security#scenario
Q106
Interviewer asks: "Your test passes in IDE but fails in Maven." What are likely causes?
intermediate
Differences in classpath, working directory, JVM args, locale/time zone, test order or parallelism, unsynchronized shared state, or the test not following Surefire's naming conventions (so not run at all).
IDE may compile resources differently; Maven filters resources or uses profile properties.
Set -Duser.timezone and file.encoding consistently via argLine.
Run mvn -Dtest=MyTest test to reproduce; check target/surefire-reports.
Mockito inline agent warnings or Java version mismatch between IDE and Maven toolchain.
⚠ Follow-up traps
Why would Maven run zero tests? Names not matching *Test, or missing JUnit 5 provider/Jupiter engine on classpath.
Is the working directory identical? Not necessarily; Surefire uses the module base dir.
#maven#surefire#environment#scenario
Q107
When do you prefer a Fake over a Mock for a repository in service tests?
intermediate
Prefer a fake (in-memory map-backed repository) when many tests exercise save-then-read flows: tests stay about outcomes, stubs are not repeated, and refactoring call patterns does not break them.
Use mocks for interaction-only boundaries such as email, payment gateways or message publishers.
A fake needs its own tests or contract tests to ensure it behaves like the real implementation.
Spring Data repositories are interfaces, so a fake needs full implementation of the used methods.
⚠ Follow-up traps
What is the risk with fakes? They can diverge from real behaviour (ordering, constraints) and hide bugs; run contract tests against both.
Why not always use Testcontainers instead? It is slower; fakes keep unit tests fast.
#fake#test-doubles#trade-offs#scenario
Q108
A new teammate asks whether to use `@SpringBootTest` for everything. What do you advise?
basic
No. Default to plain unit tests for logic, add slices for web/persistence layers, and keep a small set of @SpringBootTest tests for end-to-end wiring.
Full-context tests start slowly, fail with large unrelated diffs and give vague failures.
A slice test fails only when its layer is wrong, so diagnosis is faster.
Keep one smoke test that the context loads (contextLoads) to catch wiring problems.
⚠ Follow-up traps
Is contextLoads enough coverage? No, it only proves the wiring starts.
Can slices detect missing beans for other layers? No, the full context test does.