386 Java Developer Interview Questions & Answers

139 top • 34 Amazon • 36 Apple • 41 Google • 35 Meta • 39 Microsoft • 31 Netflix • 31 NVIDIA

Java Developer icon

Questions with Detailed ExplanationsWith Detailed Explanations

(Last Updated: August 28, 2026)

121. What makes a good assertion in a unit test?TestingMedium

Question Details

Explain what makes an assertion meaningful, readable, and resilient to implementation details.

Short Interview Answer (30-60 seconds)

I assert the observable behavior that the unit promises, not its private fields, internal collections, or internal call order. I arrange deterministic input, call the public behavior, and compare the actual result with a precise expected result. The assertion should be readable and should fail for one clear behavioral reason. Multiple assertion calls are acceptable when they describe the same outcome. I verify an interaction only when it is part of the observable contract. This keeps the test useful during refactoring, but it does not prove that real integrations work.

Detailed Explanation

See the Code while reading this explanation.

A good assertion checks whether the result that matters is correct. It should tell the reader what was expected and make a failure easy to understand. It should focus on something visible to the caller instead of hidden fields, private steps, or internal collections. This allows developers to change the inside of the code without breaking the test when the visible result stays the same. The test should also use stable input so that it gives the same answer each time it runs.

Useful Questions to Ask the Interviewer
  1. Which public behavior should the test protect?
  2. Is any interaction with another component part of the required contract?
  3. Are time, random values, or external inputs involved?
  4. Should this remain a unit test, or does it require an integration test?
What makes a good assertion in a unit test? diagram
How to Explain It in an Interview

Start with the public behavior that matters. In this example, OrderService is the system under test and calculateTotal is the public action. The unit test creates a deterministic Order, calls calculateTotal, and compares the returned Money value with the expected discounted total.

The unit test boundary contains OrderService and its simple input values. A database, network service, system clock, or other external system stays outside this test unless the production behavior directly depends on it. A required collaborator should be replaced only at the same constructor, method, or interface boundary used by the production code.

A meaningful assertion checks an observable business result. Here, the result is the total returned to the caller. A readable and specific assertion shows the expected value, the actual value, and a useful message such as discounted total. The test should have one behavioral reason to fail. It may contain several assertion calls when they all describe the same outcome.

A resilient assertion depends on the public contract rather than private fields, internal collections, or internal call order. For example, checking service.getAppliedDiscounts().size() would couple the test to an internal collection. That test could fail after a safe refactoring even when calculateTotal still returns the correct result.

Interaction verification is optional. It is appropriate only when the interaction itself is part of the observable contract, such as publishing a required event. Verifying every internal call makes a test harder to change and does not add useful confidence.

The test must also remain deterministic. It should use fixed input values and control time, randomness, environment data, and external responses when those values affect the result. Each test should create its own small fixture and should not depend on execution order or shared mutable state.

Separate tests should cover important failure and boundary cases that belong to the contract, such as invalid input, an empty order, or an invalid discount. A normal in memory unit test usually needs no special cleanup because each test creates fresh local data. Any temporary resource or background task must still be closed before the test ends.

The test should run through the project Maven or Gradle wrapper in local development and CI. A passing unit test gives confidence in the isolated behavior. It does not prove that a real database, message broker, framework configuration, or network integration works. Those boundaries require separate integration tests.

Key Insight / Why This Solution Works
  1. Define the public behavior that callers depend on.
  2. Choose a unit test when one small behavior can be checked with external dependencies isolated.
  3. Arrange small and deterministic input data.
  4. Replace only collaborators that cross the unit boundary, using the same injection point as production code.
  5. Call the public method under test.
  6. Compare the observable result with a precise expected value.
  7. Add a useful assertion message when it gives important failure context.
  8. Verify an interaction only when that interaction belongs to the observable contract.
  9. Put separate failure and boundary cases in separate tests.
  10. Keep test state isolated and run the test through the project build in CI.
Code
import static org.junit.jupiter.api.Assertions.assertEquals;

import java.math.BigDecimal;
import org.junit.jupiter.api.Test;

class OrderServiceTest {

    @Test
    void returnsDiscountedTotal() {
        OrderService service = new OrderService();
        Order order = new Order(new Money(100), new Money(10));

        Money total = service.calculateTotal(order);

        assertEquals(new Money(90), total, "discounted total");
    }

    static final class OrderService {

        Money calculateTotal(Order order) {
            return order.subtotal().subtract(order.discount());
        }
    }

    record Order(Money subtotal, Money discount) {}

    record Money(BigDecimal amount) {
        Money(int amount) {
            this(BigDecimal.valueOf(amount));
        }

        Money subtract(Money other) {
            return new Money(amount.subtract(other.amount));
        }
    }
}
Why Interviewers Ask This

Interviewers ask this question to see whether a candidate can separate useful behavior checks from fragile checks of internal code. They want evidence that the candidate can choose an observable result, write a precise and readable expectation, keep the test deterministic, and avoid tests that break during harmless refactoring. The question also tests whether the candidate knows that an interaction should be verified only when that interaction is part of the observable contract.

Common interview mistakes

A common mistake is checking private fields, internal collections, or exact internal call order instead of the public result. Another mistake is using a broad boolean assertion when a precise equality or exception assertion would explain the expectation better. Some tests contain several unrelated expectations, so one failure does not reveal which behavior is wrong. Other tests verify every mock interaction even when the interaction is not part of the contract. Tests also become unreliable when they use current time, random values, shared mutable fixtures, external networks, or execution order without control. A passing mocked unit test must not be treated as proof that a real integration works.

Interview tip

Explain the answer with one concrete flow. Name the public behavior, show deterministic input, call the public method, and assert the observable result. Contrast that with one fragile check of a private field or internal collection. Mention that interaction verification is used only when the interaction belongs to the contract. Finish by explaining that real integration behavior requires a separate integration test.

Interviewer may ask next
How would you test this behavior if the result depended on the current time and the test sometimes failed around midnight?

I would keep OrderService as the unit test boundary and inject a Clock or another small time provider into it. The test would replace that exact dependency with a fixed clock and assert the public result for a known instant. This matters because the result no longer depends on the machine clock or the time when CI runs. The tradeoff is a small production design change, but it gives deterministic tests and makes time based rules easier to understand.

When should an interaction check move from a unit test to an integration test?

It should move to an integration test when the important question is whether real components collaborate correctly rather than whether the isolated unit requested the interaction. A unit test can verify that OrderService calls an injected event publisher when publishing is part of its observable contract. A separate integration test should use the real supported messaging or framework boundary to verify configuration, serialization, routing, and delivery. The tradeoff is that the integration test is slower and needs more setup, but it provides confidence that a mock cannot provide.

122. How do you decide what to mock in a Java unit test?TestingMedium

Question Details

Explain how you decide which dependencies to mock and which ones to keep real.

Short Interview Answer (30-60 seconds)

I mock a collaborator when it crosses the unit boundary, is slow or non deterministic, is difficult to control, or must simulate a failure. I keep a collaborator real when it is simple, fast, deterministic, and its behavior is part of what I want to test. For example, I would mock PaymentGateway but keep DiscountPolicy real. I would assert the visible receipt or exception and verify only the important charge interaction. I would use a separate integration test to check the real payment connection.

Detailed Explanation

See the Code while reading this explanation.

This question asks how to choose which helpers should be replaced during a small test. A helper should usually be replaced when it talks to an outside system, changes unpredictably, takes too long, or is difficult to force into a failure. A helper should usually stay real when it is simple, quick, stable, and contains behavior that the test should exercise. The goal is to test one clear result while keeping the test fast, repeatable, and useful.

Useful Questions to Ask the Interviewer
  1. What public behavior should this test prove?
  2. Which collaborators cross the boundary of the class?
  3. Which collaborator behavior should remain real?
  4. Which success or failure result must the test control?
  5. Which real integration needs a separate test?
How do you decide what to mock in a Java unit test? diagram
How to Explain It in an Interview

I first define the unit as OrderService.placeOrder. This is a unit test because it checks one small behavior while isolating an external collaborator.

OrderService receives PaymentGateway and DiscountPolicy through its constructor. PaymentGateway represents an external payment boundary, so I replace that exact constructor argument with a Mockito mock. This lets the test control whether charging succeeds or fails without using a real network or payment provider.

I keep DiscountPolicy real because it is simple, fast, deterministic, and its price calculation is behavior that I want the test to exercise. Mocking it would remove useful behavior and could make the test depend too much on internal implementation details.

Each test creates its own PaymentGateway mock, DiscountPolicy, OrderService, Order, and expected payment result. These are per test fixtures, so tests do not share mutable state and do not depend on execution order.

For the success case, I configure gateway.charge to return an approved PaymentResult. I call service.placeOrder, assert the visible receipt amount, and verify that gateway.charge received the expected order and price.

For the failure case, I configure the same gateway boundary to return a declined PaymentResult. I call the public method and assert that PaymentException is thrown. I also verify the important charge interaction.

No cleanup or rollback is needed because this unit test creates no database rows, files, network connections, or background tasks. The tests remain deterministic because all external payment behavior is controlled by the mock.

These tests can run in CI with the project Maven or Gradle wrapper. They should run quickly and independently. However, they do not prove that the real payment integration works. A separate integration or contract test must check real wiring, authentication, serialization, network behavior, and provider compatibility.

Key Insight / Why This Solution Works
  1. Define the public behavior under test as OrderService.placeOrder.
  2. List the collaborators used by OrderService.
  3. Identify which collaborators cross the unit boundary or are difficult to control.
  4. Replace PaymentGateway at its constructor injection point with a Mockito mock.
  5. Keep DiscountPolicy real because it is simple, deterministic, and part of the behavior being tested.
  6. Create fresh test data and collaborators inside each test method.
  7. Configure gateway.charge to return an approved or declined PaymentResult.
  8. Call service.placeOrder.
  9. Assert the visible Receipt result or expected PaymentException.
  10. Verify only the important gateway.charge interaction.
  11. Use a separate integration or contract test for the real payment connection.
Code
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import java.math.BigDecimal;
import org.junit.jupiter.api.Test;

class OrderServiceTest {

    @Test
    void placeOrder_success() {
        PaymentGateway gateway = mock(PaymentGateway.class);
        DiscountPolicy discountPolicy = new DiscountPolicy();
        OrderService service = new OrderService(gateway, discountPolicy);

        Order order = new Order("A123");
        BigDecimal price = new BigDecimal("100.00");
        PaymentResult success = new PaymentResult(true, "OK");

        when(gateway.charge(order, price)).thenReturn(success);

        Receipt receipt = service.placeOrder(order);

        assertEquals(price, receipt.amount());
        verify(gateway).charge(order, price);
    }

    @Test
    void placeOrder_paymentFails_throwsException() {
        PaymentGateway gateway = mock(PaymentGateway.class);
        DiscountPolicy discountPolicy = new DiscountPolicy();
        OrderService service = new OrderService(gateway, discountPolicy);

        Order order = new Order("A124");
        BigDecimal price = new BigDecimal("100.00");
        PaymentResult failure = new PaymentResult(false, "Declined");

        when(gateway.charge(order, price)).thenReturn(failure);

        assertThrows(PaymentException.class, () -> service.placeOrder(order));
        verify(gateway).charge(order, price);
    }

    interface PaymentGateway {
        PaymentResult charge(Order order, BigDecimal amount);
    }

    static final class DiscountPolicy {

        BigDecimal apply(Order order) {
            return new BigDecimal("100.00");
        }
    }

    static final class OrderService {

        private final PaymentGateway gateway;
        private final DiscountPolicy discountPolicy;

        OrderService(PaymentGateway gateway, DiscountPolicy discountPolicy) {
            this.gateway = gateway;
            this.discountPolicy = discountPolicy;
        }

        Receipt placeOrder(Order order) {
            BigDecimal price = discountPolicy.apply(order);
            PaymentResult result = gateway.charge(order, price);

            if (!result.approved()) {
                throw new PaymentException("Payment failed");
            }

            return new Receipt(order.id(), price);
        }
    }

    record Order(String id) {}

    record PaymentResult(boolean approved, String message) {}

    record Receipt(String orderId, BigDecimal amount) {}

    static final class PaymentException extends RuntimeException {

        PaymentException(String message) {
            super(message);
        }
    }
}
Why Interviewers Ask This

Interviewers ask this question to evaluate whether a candidate can define a sensible unit test boundary. They want to see whether the candidate can isolate external or difficult collaborators without replacing useful business behavior. The question also tests whether the candidate understands deterministic tests, constructor based dependency replacement, visible behavior assertions, interaction verification, failure simulation, and the limits of mocked tests.

Common interview mistakes

Common mistakes include mocking value objects, simple collections, or deterministic business rules that should remain real. Another mistake is replacing the wrong dependency instead of the collaborator that OrderService actually receives and calls. Over verifying every internal interaction can make tests fragile. Keeping a real network or payment provider in a unit test can make the test slow and flaky. Weak assertions, ignored failure paths, shared mutable fixtures, test order dependencies, and treating a mocked test as proof of real integration are also common problems.

Interview tip

Start by naming the unit boundary. Then say what you would mock, what you would keep real, and why. Use one consistent example: mock PaymentGateway, keep DiscountPolicy real, assert the receipt or exception, verify the charge call, and mention the separate integration test.

Interviewer may ask next
How would you test a payment timeout without making the unit test flaky?

I would keep PaymentGateway as the mocked constructor boundary and configure it to throw the timeout exception or return the timeout result defined by the production contract. I would then call OrderService.placeOrder and assert the exact visible response, such as PaymentException. This keeps the test deterministic because it does not use a real network or a fixed sleep. The tradeoff is that the unit test proves only how OrderService reacts to the timeout contract. It does not prove that the real client timeout configuration works.

When should the PaymentGateway be real instead of mocked?

PaymentGateway should be real in an integration or contract test when the goal changes from testing OrderService behavior to testing the payment adapter, wiring, authentication, serialization, request format, or provider compatibility. The test boundary then includes the real adapter and controlled test infrastructure. This provides stronger integration confidence, but it requires more setup, runs more slowly, and may need a sandbox service, container, or dedicated CI configuration.

123. How do you test exceptions and edge cases with JUnit 5?TestingMedium

Question Details

Explain how you would write tests for failure paths, boundary values, and error handling in JUnit 5.

Short Interview Answer (30-60 seconds)

I write a separate JUnit 5 test for each expected outcome. I use assertThrows to verify the specific exception type and capture the exception when I need to check a stable message or error detail. I use parameterized tests for valid and invalid boundary values. I also test the normal path with a direct result assertion. In this example, CalculatorService is the complete unit test boundary. It has no external dependencies, so each test creates a fresh instance and needs no mock or cleanup.

Detailed Explanation

See the Code while reading this explanation.

The goal is to check what a small piece of code does when it receives normal, missing, or out of range input. Each test should describe one situation and one expected result. This makes failures easy to understand. For example, dividing by zero should produce the expected error, while valid division should return the expected number. For a text position, test the first and last allowed positions, then test values immediately outside the allowed range. Missing text should be checked separately. The tests should not share changing data or depend on execution order.

Useful Questions to Ask the Interviewer
  1. Is the exact exception message part of the public contract?
  2. Which minimum, maximum, empty, null, and out of range values matter for these methods?
  3. Should this remain a unit test, or does any behavior depend on a real external component?
How do you test exceptions and edge cases with JUnit 5? diagram
How to Explain It in an Interview

I first list the observable behaviors of CalculatorService. The divide method should return the quotient for valid input. A zero divisor should throw ArithmeticException. The indexAt method should throw IllegalArgumentException for null text. It should throw IndexOutOfBoundsException when the index is below zero or greater than or equal to the text length. It should return the correct character for the first and last valid indexes.

These are unit tests because the test boundary contains only CalculatorService and its public methods. There is no database, network service, clock, file, or other collaborator. Each test creates a fresh CalculatorService instance, so the fixture is small and independent. No mock, stub, fake, spy, dependency replacement, or shared setup is needed.

For failure paths, I use assertThrows with the most specific expected exception class. The method returns the thrown exception, so I can inspect a stable message when the message is part of the contract. I avoid asserting every message by default because harmless wording changes can otherwise break the test without changing the real behavior.

For valid index boundaries, I use a parameterized test with indexes zero and two for the text abc. These are the first and last valid positions. For invalid boundaries, I use a separate parameterized test with minus one and three. Minus one is immediately below the minimum. Three is the exclusive upper boundary because the length of abc is three. Keeping valid and invalid outcomes in separate test methods makes failures easier to read and diagnose.

I also test valid division with assertEquals. This proves that the method completed successfully and returned the expected value. An additional assertDoesNotThrow call would be unnecessary because assertEquals already fails if the method throws.

No cleanup is required because the tests create only local in memory objects and do not open resources or change shared state. The tests can run in any order. They remain deterministic because all values are fixed and there is no time, randomness, network access, or environment dependency.

In continuous integration, I run the tests with the project wrapper, such as ./mvnw test or ./gradlew test, and use the JUnit Jupiter dependencies declared by the project.

The main tradeoff is how much error detail to assert. Checking only the exception type gives a stable test but may miss an important public error contract. Checking the exact message gives stronger contract coverage but increases maintenance when wording changes. I check a message only when callers or users are expected to rely on it.

Key Insight / Why This Solution Works
  1. List the normal behavior, failure paths, and important boundaries.
  2. Keep CalculatorService as the unit test boundary.
  3. Create a fresh CalculatorService instance inside each test.
  4. Test valid division with assertEquals.
  5. Test division by zero with assertThrows and capture ArithmeticException.
  6. Check the division error message because the example treats it as a stable contract detail.
  7. Test the first and last valid indexes with one parameterized test.
  8. Test one value below the minimum and the exclusive upper boundary with a separate parameterized exception test.
  9. Test null text in its own method and assert IllegalArgumentException.
  10. Keep each test focused on one expected outcome.
  11. Run the suite with the project Maven or Gradle wrapper in continuous integration.
Code
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.ValueSource;

class CalculatorServiceTest {

    @Test
    void divideByZeroThrowsArithmeticException() {
        CalculatorService service = new CalculatorService();

        ArithmeticException exception = assertThrows(ArithmeticException.class, () ->
            service.divide(10, 0)
        );

        assertEquals("Division by zero", exception.getMessage());
    }

    @Test
    void validDivisionReturnsQuotient() {
        CalculatorService service = new CalculatorService();

        assertEquals(5, service.divide(10, 2));
    }

    @ParameterizedTest
    @CsvSource({ "0, a", "2, c" })
    void validIndexBoundariesReturnCharacter(int index, char expected) {
        CalculatorService service = new CalculatorService();

        assertEquals(expected, service.indexAt("abc", index));
    }

    @ParameterizedTest
    @ValueSource(ints = { -1, 3 })
    void invalidIndexBoundariesThrowException(int index) {
        CalculatorService service = new CalculatorService();

        assertThrows(IndexOutOfBoundsException.class, () -> service.indexAt("abc", index));
    }

    @Test
    void nullTextThrowsIllegalArgumentException() {
        CalculatorService service = new CalculatorService();

        IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () ->
            service.indexAt(null, 0)
        );

        assertEquals("text must not be null", exception.getMessage());
    }

    static final class CalculatorService {

        int divide(int a, int b) {
            if (b == 0) {
                throw new ArithmeticException("Division by zero");
            }
            return a / b;
        }

        char indexAt(String text, int index) {
            if (text == null) {
                throw new IllegalArgumentException("text must not be null");
            }
            if (index < 0 || index >= text.length()) {
                throw new IndexOutOfBoundsException();
            }
            return text.charAt(index);
        }
    }
}
Why Interviewers Ask This

Interviewers ask this question to see whether a candidate tests more than the normal path. They want to know whether the candidate can identify meaningful failure conditions and boundary values, choose the correct JUnit 5 assertion, and keep tests focused and repeatable. The question also checks whether the candidate understands test isolation, stable error contracts, parameterized tests, and the difference between asserting useful public behavior and making a test depend on private implementation details.

Common interview mistakes

Common mistakes include testing only successful input, using assertThrows with a broad Exception type, combining valid and invalid outcomes in one conditional test, and checking every exception message even when it is not a stable contract. Another mistake is choosing invalid values far away from the boundary while missing the values immediately beside the limit. Tests can also become misleading when they share mutable state, depend on execution order, or assert private implementation details instead of public behavior. Using assertDoesNotThrow without checking the returned result is weaker when a direct result assertion is available.

Interview tip

Explain the cases as a small decision table. Name the normal case, each failure path, and the exact boundary values. Then show which JUnit 5 assertion handles each outcome. State that every test has one expected result, uses fresh state, and checks an error message only when it is part of the contract.

Interviewer may ask next
How would you test the exception if its message may change?

I would keep the unit test boundary around CalculatorService and always assert the specific exception type. I would assert the full message only when callers rely on that exact text as part of the contract. Otherwise, I would check a more stable property, such as an error code, cause, or structured field, if the exception provides one. This matters because a full message assertion can fail after a harmless wording change. The tradeoff is that a less detailed assertion is more stable but provides less protection for user visible error text.

When should these checks become integration tests instead of unit tests?

They should become integration tests when the behavior depends on a real component outside CalculatorService, such as a validation framework, serializer, database constraint, or remote contract. The current boundary is a unit test because the methods use only local input and local logic. For an integration test, I would keep the required real component inside the boundary and use controlled infrastructure with isolated data. This matters because a unit test cannot prove that framework configuration or a real external contract works. The tradeoff is slower setup and longer continuous integration time in exchange for higher confidence at that boundary.

124. How do you write parameterized tests in JUnit?TestingMedium

Question Details

Explain how parameterized tests reduce duplication and how you would structure them.

Short Interview Answer (30-60 seconds)

I write one JUnit Jupiter test method with @ParameterizedTest and connect it to a data source such as @CsvSource, @ValueSource, @EnumSource, or @MethodSource. The method parameters receive one argument set at a time. JUnit runs the same test logic once for every set and reports each invocation separately. This removes repeated test methods and makes new cases easy to add. The main tradeoff is that all rows should test the same behavior. Cases with different setup or assertions should use separate tests.

Detailed Explanation

See the Code while reading this explanation.

A parameterized test checks the same rule with several examples without copying the complete test many times. We write the test steps once, then provide different input values and expected results. The test runner repeats those steps for each group of values and reports every result separately. This keeps the test smaller, makes new cases easier to add, and helps cover normal values, zero, negative values, and boundary values in a consistent way.

Useful Questions to Ask the Interviewer
  1. Which JUnit major version does the project use?
  2. Are the values simple constants or do they need to be created by code?
  3. Which normal, boundary, and invalid cases must be covered?
  4. Should each invocation have a descriptive display name?
How do you write parameterized tests in JUnit? diagram
How to Explain It in an Interview

The system under test in the diagram is Calculator.add(int, int). This is a unit test because it checks one small deterministic method. The method does not use a database, network, file, clock, or another external dependency.

The test method uses @ParameterizedTest instead of @Test. The @CsvSource annotation supplies four rows. Each row contains a, b, and expected. JUnit reads one row, passes those values into addReturnsSum, creates a Calculator, calls calculator.add(a, b), and checks the returned value with assertEquals(expected, actual).

JUnit repeats that flow once for every row. The four rows check positive values, a negative value, zero, and a larger mixed sign case. The display name uses {index}, {0}, {1}, and {2}, so each invocation can show its number, arguments, and expected result.

The test boundary contains the Calculator object, the supplied arguments, the public add method, and the result assertion. No dependency replacement is needed because the class has no external collaborator. No cleanup or rollback is needed because the test creates no persistent state. Each invocation is independent and does not depend on execution order.

Parameterized tests reduce duplication because the setup, action, and assertion appear once. Adding another case normally means adding one data row instead of copying another test method. JUnit still reports every invocation separately, so the exact failing row can be identified.

The data source should match the data shape. @ValueSource is useful for one simple argument. @EnumSource supplies enum constants. @CsvSource is useful for small inline rows with several values. @MethodSource is useful for generated values or complex objects. @CsvFileSource can read rows from a classpath CSV file.

The main tradeoff is readability. Parameterization works well when every row exercises the same behavior with the same setup, action, and assertion. It becomes harder to understand when rows require different workflows or unrelated assertions. Those cases should normally be separated into clearly named tests.

The test is deterministic because it uses fixed input values and no external state. It can run locally and in CI through the project Maven or Gradle wrapper. The number of invocations grows with the number of argument sets, so the data should remain focused and meaningful.

Technical Approach
  1. Identify one behavior that must be checked with several inputs.
  2. Annotate one test method with @ParameterizedTest.
  3. Choose a data source that matches the required arguments.
  4. Declare method parameters in the same order as the supplied values.
  5. Add normal, zero, negative, and boundary cases to the source.
  6. Create the Calculator fixture for the current invocation.
  7. Call calculator.add(a, b).
  8. Compare the actual result with expected.
  9. Let JUnit repeat the method and report every invocation separately.
  10. Keep cases with different behavior or assertions in separate tests.
Practical Insights

Traditional algorithmic complexity is not the main concern. If the source contains n argument sets, JUnit invokes the test method n times. The total runtime is approximately the cost of one invocation multiplied by n. The inline data uses little memory. Maintenance cost is usually lower because the setup, action, and assertion are written once. A very large data source can increase CI time and make failures harder to review, so each row should represent a useful case.

Code
import static org.junit.jupiter.api.Assertions.assertEquals;

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

class CalculatorTest {

    @ParameterizedTest(name = "{index} => add({0}, {1}) = {2}")
    @CsvSource({ "1, 2, 3", "-1, 1, 0", "0, 0, 0", "100, -50, 50" })
    void addReturnsSum(int a, int b, int expected) {
        Calculator calculator = new Calculator();

        int actual = calculator.add(a, b);

        assertEquals(expected, actual);
    }
}

class Calculator {

    int add(int a, int b) {
        return a + b;
    }
}
Why Interviewers Ask This

Interviewers ask this question to check whether a candidate can remove repeated test code, organize several input cases clearly, choose the correct JUnit data source, and keep tests readable and deterministic. They also want to confirm that the candidate understands that JUnit creates a separate test invocation and result for every supplied argument set.

Common interview mistakes

Common mistakes include copying several nearly identical test methods, using a parameterized test for cases that have different behavior, placing source values in the wrong order, declaring incompatible parameter types, and using weak data that misses important boundaries. Other mistakes include sharing mutable state between invocations, hiding complex setup inside an unreadable source, using unclear display names, adding too many unrelated rows, and assuming that more rows automatically mean better test quality.

Interview tip

Start by saying that you write the test logic once and supply many argument sets. Name @ParameterizedTest and one suitable source such as @CsvSource. Explain that the parameters receive one row at a time and that JUnit reports each invocation separately. Finish by stating that all rows should test the same behavior.

Interviewer may ask next
How would you parameterize invalid inputs that should throw an exception?

I would keep the same unit test boundary but create a separate parameterized test method for invalid inputs. Its data source would contain only values that should fail, and each invocation would use assertThrows to verify the expected exception type. I would not mix successful results and exception cases in the same method because they use different assertions and represent different behavior. The additional method adds a small amount of code, but it gives clearer intent and failure reports.

When would you use @MethodSource instead of @CsvSource?

I would use @MethodSource when the arguments contain complex objects, generated values, or setup that is difficult to express as CSV text. The test boundary and assertion remain unchanged, but a source method returns the argument sets. This provides stronger typing and more flexibility. The tradeoff is additional supporting code, so I would keep @CsvSource for small readable scalar values and choose @MethodSource only when the data needs programmatic creation.

125. How do you test code that depends on a database?TestingMedium

Question Details

Explain approaches for testing database-dependent code, including test data setup and cleanup.

Short Interview Answer (30-60 seconds)

I test database dependent code at two complementary levels. For service business rules, I use a unit test and replace the injected repository with a Mockito mock. I configure controlled repository results, call the public service method, assert the returned value or exception, and verify important repository interactions. For queries, mappings, constraints, migrations, and transactions, I use an integration test with an isolated real database such as PostgreSQL in Testcontainers. I apply migrations, create only the required test data, run the behavior, assert the stored state, and reset the database with rollback, truncation, schema reset, or container recreation. I never use the production database.

Detailed Explanation

See the Code while reading this explanation.

This question asks how to check code that reads or writes stored information without changing real information or allowing one test to affect another. The main decision is whether a test only needs to check a service rule or must also check real storage behavior. A good answer should explain how test data is created, how the action is run, what result is checked, how failure cases are covered, and how stored state is removed or reset. It should also explain why fast isolated tests and slower realistic tests are both useful.

Useful Questions to Ask the Interviewer
  1. Are we testing only service rules, or must we also test real queries and database rules?
  2. Which database engine does production use?
  3. Does the project already use Testcontainers or another isolated test database?
  4. Should each test use rollback, table truncation, a new schema, or a new container?
  5. Are migrations part of the behavior that must be tested?
How do you test code that depends on a database? diagram
How to Explain It in an Interview

I begin by defining the behavior of UserService.registerUser(email). When the email is not present, the service saves a new user and returns it. When the email already exists, the service throws DuplicateEmailException and does not save another user.

For these service rules, I use a unit test. UserService receives UserRepository through its constructor, so UserRepository is the correct dependency replacement boundary. I create a Mockito mock, pass it into UserService, and configure findByEmail to return controlled data. For the success test, it returns Optional.empty. I call registerUser, assert the returned email, and verify that save was called. For the duplicate email test, findByEmail returns an existing user. I assert DuplicateEmailException and verify that save was never called.

This unit test is fast and deterministic because it does not start a database. It proves the service decision and the important repository interaction. It does not prove SQL, object mapping, schema constraints, migrations, transactions, or real database behavior.

For those concerns, I use an integration test with UserService, the real UserRepository implementation, and an isolated PostgreSQL test database. Testcontainers is a practical option because it starts a real database engine in a controlled environment. I apply the required Flyway or Liquibase migrations, create only the data needed by the test, run registerUser, and query the repository or database to confirm that the row was stored correctly. I also test that duplicate data is rejected by the expected service rule or database constraint.

Test data should be small, clear, and scoped to the test that needs it. A factory or builder is useful when tests need several data variations. Tests must not depend on execution order or on mutable records shared with other tests.

Cleanup depends on the isolation strategy. A transaction can be rolled back after each test when all relevant work participates in that transaction. Otherwise, the test can truncate affected tables, reset an isolated schema, or recreate the container. The important rule is that every test starts from a known state.

In continuous integration, I run unit tests first because they are fast. Integration tests can run in a later task where Docker or another supported container runtime is available. Database startup and migrations add time, so a team may reuse an isolated container while resetting its data between tests. Production data must never be used.

The main tradeoff is speed versus confidence. Mocked repository tests provide fast feedback about business rules. Real database tests take longer, but they catch query, mapping, constraint, migration, and transaction problems that a mock cannot detect.

Key Insight / Why This Solution Works
  1. Define the expected behavior of UserService.registerUser(email), including the successful registration result and the duplicate email failure.
  2. Choose the test level. Use a unit test for service business rules and an integration test for real database behavior.
  3. In the unit test, create a Mockito mock of the constructor injected UserRepository.
  4. Configure findByEmail to return Optional.empty for the successful registration test.
  5. Configure save to return the User object passed to it.
  6. Call registerUser, assert the returned email, and verify that save was called.
  7. In a separate failure test, configure findByEmail to return an existing user.
  8. Assert DuplicateEmailException and verify that save was never called.
  9. For the integration test, start an isolated PostgreSQL test database and apply the required migrations.
  10. Create only the fixture data needed by the test.
  11. Run the service method and assert the stored row, query result, mapping, constraint, and transaction behavior that matters.
  12. Reset database state with rollback, truncation, schema reset, or container recreation.
  13. Run tests independently in continuous integration and never connect them to the production database.
Code
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import java.util.Optional;
import org.junit.jupiter.api.Test;

class UserServiceTest {

    @Test
    void registersUserWhenEmailDoesNotExist() {
        UserRepository repo = mock(UserRepository.class);
        UserService service = new UserService(repo);

        when(repo.findByEmail("a@x.com")).thenReturn(Optional.empty());
        when(repo.save(any(User.class))).thenAnswer(invocation -> invocation.getArgument(0));

        User user = service.registerUser("a@x.com");

        assertEquals("a@x.com", user.getEmail());
        verify(repo).save(any(User.class));
    }

    @Test
    void rejectsDuplicateEmailWithoutSaving() {
        UserRepository repo = mock(UserRepository.class);
        UserService service = new UserService(repo);
        User existingUser = new User("a@x.com");

        when(repo.findByEmail("a@x.com")).thenReturn(Optional.of(existingUser));

        assertThrows(DuplicateEmailException.class, () -> service.registerUser("a@x.com"));

        verify(repo, never()).save(any(User.class));
    }
}

interface UserRepository {
    Optional<User> findByEmail(String email);

    User save(User user);
}

final class UserService {

    private final UserRepository repository;

    UserService(UserRepository repository) {
        this.repository = repository;
    }

    User registerUser(String email) {
        if (repository.findByEmail(email).isPresent()) {
            throw new DuplicateEmailException(email);
        }

        return repository.save(new User(email));
    }
}

final class User {

    private final String email;

    User(String email) {
        this.email = email;
    }

    String getEmail() {
        return email;
    }
}

final class DuplicateEmailException extends RuntimeException {

    DuplicateEmailException(String email) {
        super("Email already exists: " + email);
    }
}
Why Interviewers Ask This

Interviewers ask this question to evaluate whether the candidate can choose the correct test boundary and balance speed with confidence. They want to see sound judgment about unit tests, integration tests, test data, cleanup, isolation, failure cases, deterministic behavior, and continuous integration. They also expect the candidate to understand that a mocked repository can test service rules but cannot prove queries, mappings, constraints, migrations, or transactions.

Common interview mistakes

A common mistake is mocking database library or ORM internals instead of replacing the application owned UserRepository boundary. Another mistake is treating a passing Mockito test as proof that SQL, mappings, constraints, migrations, or transactions work. Some tests connect to a shared development database or depend on data left by another test, which makes the results unreliable. Other problems include using production data, depending on test order, creating large shared fixtures, failing to test the duplicate email path, checking only that no exception occurred, and forgetting to reset database state after an integration test.

Interview tip

Explain the answer as two complementary levels. First, show how a mocked UserRepository proves the UserService business rules. Then explain that a real isolated test database is required for queries, mappings, constraints, migrations, and transactions. Finish with test data setup, cleanup, isolation, continuous integration cost, and the rule that tests never use the production database.

Interviewer may ask next
What would you do if database integration tests pass alone but fail when the whole test suite runs?

I would treat that as a state isolation problem at the real database test boundary. I would check for shared rows, reused identifiers, missing rollback, incomplete table truncation, test order assumptions, and transactions that finish after the test. Each test should create only its required data and start from a known database state. I would use rollback when all work joins the same transaction. Otherwise, I would truncate the affected tables, reset an isolated schema, or recreate the container. Stronger isolation adds runtime cost, but it prevents one test from changing another test's result.

Should every repository test use a new PostgreSQL container?

No. The required boundary is an isolated real test database, not necessarily a new container for every test method. A test suite can reuse one Testcontainers PostgreSQL instance to reduce startup time, provided each test receives clean state through rollback, truncation, or an isolated schema. A new container gives stronger isolation but increases continuous integration time. Container reuse is faster, while recreation gives simpler state guarantees. The team should choose the lightest reset strategy that still keeps tests deterministic.

126. How do you keep tests isolated and deterministic?TestingMedium

Question Details

Explain how you avoid flaky tests and make test outcomes repeatable.

Short Interview Answer (30-60 seconds)

I give every test its own objects and data, replace external boundaries through constructor injection, and control anything that can change between runs, such as time, randomness, background work, files, and global settings. In this unit test, UserService stays real, UserRepository is a Mockito mock, and Clock is fixed. I call the public method, assert the visible result, verify one important interaction, and test the failure path separately. This keeps the test fast and repeatable, but separate integration tests are still needed for real database and network contracts.

Detailed Explanation

See the Code while reading this explanation.

The goal is to make every test produce the same result when it receives the same input. One test must not change another test. I create new objects and data for each case, control anything that can vary, and keep outside systems away from the small test. I check the result that a caller can observe. I also restore any setting or resource changed by the test. This makes failures easier to understand because a failed test points to one behavior instead of hidden state, timing, or another test.

Useful Questions to Ask the Interviewer
  1. Does the interviewer want only unit tests, or should I also discuss integration tests?
  2. Does the code use time, random values, files, network calls, database access, or background work?
  3. Can dependencies be supplied through a constructor or method?
  4. Are tests expected to run in parallel in continuous integration?
How do you keep tests isolated and deterministic? diagram
How to Explain It in an Interview

I first define the behavior and the test boundary. In this example, the system under test is UserService.getActiveUser. The unit test keeps UserService real. It replaces UserRepository with a Mockito mock because the repository is outside the unit boundary. It passes a fixed java.time.Clock through the same constructor used by production code, so the checkedAt value in the returned UserResult is predictable.

Each test receives fresh setup. JUnit Jupiter creates a new test instance for each test method by default, Mockito supplies a fresh repository mock through MockitoExtension, and BeforeEach creates a new UserService with that mock and the fixed Clock. The test does not use mutable static fields, shared fixtures, a real network, a production database, or the real system clock.

For the success case, the repository stub returns one active user. The test calls getActiveUser and asserts the visible result, including the user id and the exact fixed time. It also verifies the important repository lookup. For the failure case, the repository stub returns an empty Optional, and the test asserts that NoSuchElementException is thrown. These are separate tests, so one outcome does not depend on the other.

Other changing inputs should also be controlled. Random values can come from an injected generator or a generator with a known seed. Background work should expose an observable completion signal, Future, latch, or test executor instead of using Thread.sleep. Parallel execution is safe only when the production code and fixtures are thread safe and no mutable state is shared.

Cleanup depends on what the test owns or changes. A test should close test owned resources, delete temporary files or data, and restore changed locale, time zone, system properties, or other global settings. This example needs no explicit cleanup because it creates no external resource and changes no global state.

The same tests should pass when run alone, in any order, repeatedly, and in continuous integration. The project Maven or Gradle wrapper should run the declared test dependencies so local and continuous integration behavior remain consistent.

This unit test does not verify SQL, mappings, schema rules, transactions, real network timeouts, retries, or compatibility between deployed components. Those concerns need separate integration, database, contract, or end to end tests using controlled real infrastructure. The practical tradeoff is to keep most behavior tests fast and isolated, while using fewer realistic tests for boundaries that mocks cannot prove.

Key Insight / Why This Solution Works
  1. Define the visible behavior that the public method must provide.
  2. Choose the smallest correct test level. Use a unit test for UserService behavior and separate integration tests for real contracts.
  3. Create fresh test data and a fresh service for each test.
  4. Replace UserRepository at its constructor boundary with a Mockito mock.
  5. Pass a fixed Clock through the same constructor used by production code.
  6. Configure the repository stub for one success case and one failure case.
  7. Call getActiveUser through its public interface.
  8. Assert the returned user id and fixed checkedAt value.
  9. Verify the important repository lookup.
  10. Assert NoSuchElementException in a separate failure test.
  11. Close test owned resources and restore any changed global settings when required.
  12. Run the tests alone, in different orders, repeatedly, and in continuous integration.
Code
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import java.time.Clock;
import java.time.Instant;
import java.time.ZoneOffset;
import java.util.NoSuchElementException;
import java.util.Optional;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

record User(String id, boolean active) {
    boolean isActive() {
        return active;
    }
}

record UserResult(String id, Instant checkedAt) {}

interface UserRepository {
    Optional<User> findById(String id);
}

class UserService {

    private final UserRepository repo;
    private final Clock clock;

    UserService(UserRepository repo, Clock clock) {
        this.repo = repo;
        this.clock = clock;
    }

    UserResult getActiveUser(String id) {
        User user = repo.findById(id).filter(User::isActive).orElseThrow();

        return new UserResult(user.id(), clock.instant());
    }
}

@ExtendWith(MockitoExtension.class)
class UserServiceTest {

    @Mock
    UserRepository repo;

    private final Clock fixedClock = Clock.fixed(
        Instant.parse("2024-01-01T00:00:00Z"),
        ZoneOffset.UTC
    );

    private UserService service;

    @BeforeEach
    void setUp() {
        service = new UserService(repo, fixedClock);
    }

    @Test
    void returnsActiveUserAtFixedTime() {
        User user = new User("u1", true);
        when(repo.findById("u1")).thenReturn(Optional.of(user));

        UserResult result = service.getActiveUser("u1");

        assertEquals("u1", result.id());
        assertEquals(Instant.parse("2024-01-01T00:00:00Z"), result.checkedAt());
        verify(repo).findById("u1");
    }

    @Test
    void missingUserThrows() {
        when(repo.findById("u1")).thenReturn(Optional.empty());

        assertThrows(NoSuchElementException.class, () -> service.getActiveUser("u1"));
    }
}
Why Interviewers Ask This

Interviewers ask this question to see whether a candidate can create tests that produce trustworthy results. They are evaluating judgment about test boundaries, fresh data, dependency replacement, controlled inputs, meaningful assertions, cleanup, and continuous integration. They also want to know whether the candidate understands the difference between a fast isolated unit test and a realistic integration test, including what a mocked test can and cannot prove.

Common interview mistakes

Common mistakes include sharing mutable objects between tests, using static fixture state, depending on test order, reading the real clock, using uncontrolled random values, and waiting with Thread.sleep. Another mistake is replacing the wrong dependency instead of mocking the repository, client, clock, executor, or gateway at the boundary used by the production code. Tests can also become misleading when they verify too many internal calls but do not assert visible behavior. Other problems include weak failure coverage, forgotten resource cleanup, unsafe parallel execution, and claiming that a mocked unit test proves the real database or network integration.

Interview tip

Start with the rule that every test owns its state and controls every changing input. Then name the exact boundary: UserService is real, UserRepository is mocked, and Clock is fixed through constructor injection. Walk through setup, action, visible assertions, one useful interaction check, and the separate failure test. Finish by explaining that real database and network contracts still require integration tests.

Interviewer may ask next
What would you do if this test passed alone but failed when the full suite ran?

I would treat it as an isolation problem and inspect state outside the UserService unit boundary. I would check mutable static fields, reused fixtures, changed system properties, locale, time zone, temporary files, unfinished background work, and test data shared between methods. I would run the test repeatedly and in different orders to identify the leak. The correction is to create fresh fixtures, restore changed global settings, close test owned resources, and remove execution order dependencies. This matters because a test that depends on another test is not trustworthy. The tradeoff is that stronger isolation can require more explicit setup and cleanup.

When would you replace the repository mock with a real database?

I would keep the Mockito mock for the UserService unit boundary and add a separate database integration test when I need confidence in queries, mappings, constraints, transactions, or migrations. That test would use a dedicated controlled database or container, apply the required migrations, create deterministic data, and reset the database through rollback, truncation, or recreation. This matters because the unit test proves service behavior but cannot prove the database contract. The tradeoff is that the integration test gives higher fidelity but takes longer to start, run, and maintain in continuous integration.

127. How do you test asynchronous code?TestingMedium

Question Details

Explain how you would write stable tests for asynchronous behavior and timing-sensitive code.

Short Interview Answer (30-60 seconds)

I make the asynchronous behavior deterministic. I unit test ReminderService and replace the real EmailGateway with a Mockito mock. I inject a fixed Clock and a direct Executor so time and task execution are controlled. I call sendAsync, wait on the returned CompletableFuture with get and a clear timeout, and assert either the result or the exceptional cause. I test success and failure separately. I do not use Thread.sleep, and I use a separate integration test for the real gateway.

Detailed Explanation

See the Code while reading this explanation.

The goal is to check work that finishes later without waiting for an unknown amount of time. I control anything that can change, including the current time, outside communication, and where the work runs. I start the action, wait only for a clear maximum period, and check the visible result. I write separate checks for a successful result and an expected failure. This makes the checks quick, repeatable, and easier to understand when something goes wrong in a local build or an automated build.

Useful Questions to Ask the Interviewer
  1. Does the method return one final result, or can it produce several updates?
  2. Can the clock, executor, and outside service be supplied through the constructor?
  3. Should an outside timeout be returned directly, wrapped, or changed into another error?
  4. Is cancellation part of the required behavior?
How do you test asynchronous code? diagram
How to Explain It in an Interview

I would start with a unit test around ReminderService.sendAsync. ReminderService is the system under test. The real EmailGateway stays outside the unit test boundary because the test should not depend on a live network service.

I replace EmailGateway with a Mockito mock through the constructor. The mock returns a controlled success value or throws a controlled GatewayTimeoutException. I inject a fixed Clock so Instant.now always produces the same instant. I also inject Runnable::run as the Executor. This direct executor runs the submitted task on the test thread and removes unpredictable thread scheduling from this unit test.

For the success test, I configure gateway.send to return SendResult.SENT. I call service.sendAsync and capture the returned CompletableFuture. I call get with a one second timeout instead of using Thread.sleep. The timeout prevents an endless wait. I then assert that the result is SENT and verify that the gateway received user u1 and the fixed instant.

For the failure test, I configure the same gateway call to throw GatewayTimeoutException. CompletableFuture records that failure as exceptional completion. Calling get then throws ExecutionException, so I assert that exception and verify that its cause is GatewayTimeoutException. The success and failure paths belong in separate test methods because each test should describe one behavior clearly.

Each test creates its own clock, executor, mock, and service instance. There is no shared mutable fixture and no dependency on test execution order. This example creates no real thread pool, file, database, or network connection, so no cleanup is required. A test that creates a real ExecutorService must shut it down after the test so background tasks do not leak.

For retry delays, scheduled work, or backoff behavior, I would inject a controllable clock or fake scheduler and advance test time instead of waiting for real time. In continuous integration, I would run the tests through the project Maven or Gradle wrapper.

The main tradeoff is fidelity. The direct executor and mocked gateway make the unit test fast and deterministic, but they do not prove real scheduling or real network integration. I would add focused integration tests when those boundaries need verification.

Key Insight / Why This Solution Works
  1. Define the expected success and failure behavior of ReminderService.sendAsync.
  2. Choose a unit test boundary around ReminderService.
  3. Replace EmailGateway with a Mockito mock.
  4. Inject a fixed Clock and a direct Executor.
  5. Configure one gateway outcome in each test.
  6. Call sendAsync and capture the returned CompletableFuture.
  7. Wait with get and a one second timeout.
  8. Assert the successful value or exceptional cause.
  9. Verify the important gateway interaction.
  10. Shut down any real executor created by another test and run every test independently in continuous integration.
Code
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import java.time.Clock;
import java.time.Instant;
import java.time.ZoneOffset;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import org.junit.jupiter.api.Test;

class ReminderServiceTest {

    @Test
    void returnsSentWhenGatewaySucceeds() throws Exception {
        Instant fixedInstant = Instant.parse("2024-05-01T10:00:00Z");
        Clock fixedClock = Clock.fixed(fixedInstant, ZoneOffset.UTC);
        Executor direct = Runnable::run;
        EmailGateway gateway = mock(EmailGateway.class);
        ReminderService service = new ReminderService(gateway, fixedClock, direct);

        when(gateway.send("u1", fixedInstant)).thenReturn(SendResult.SENT);

        CompletableFuture<SendResult> future = service.sendAsync("u1");
        SendResult result = future.get(1, TimeUnit.SECONDS);

        assertEquals(SendResult.SENT, result);
        verify(gateway).send("u1", fixedInstant);
    }

    @Test
    void completesExceptionallyWhenGatewayTimesOut() {
        Instant fixedInstant = Instant.parse("2024-05-01T10:00:00Z");
        Clock fixedClock = Clock.fixed(fixedInstant, ZoneOffset.UTC);
        Executor direct = Runnable::run;
        EmailGateway gateway = mock(EmailGateway.class);
        ReminderService service = new ReminderService(gateway, fixedClock, direct);

        when(gateway.send("u1", fixedInstant)).thenThrow(new GatewayTimeoutException("upstream"));

        CompletableFuture<SendResult> future = service.sendAsync("u1");

        ExecutionException exception = assertThrows(ExecutionException.class, () ->
            future.get(1, TimeUnit.SECONDS)
        );

        assertTrue(exception.getCause() instanceof GatewayTimeoutException);
        assertEquals("upstream", exception.getCause().getMessage());
        verify(gateway).send("u1", fixedInstant);
    }

    interface EmailGateway {
        SendResult send(String userId, Instant requestedAt);
    }

    enum SendResult {
        SENT,
    }

    static final class GatewayTimeoutException extends RuntimeException {

        GatewayTimeoutException(String message) {
            super(message);
        }
    }

    static final class ReminderService {

        private final EmailGateway gateway;
        private final Clock clock;
        private final Executor executor;

        ReminderService(EmailGateway gateway, Clock clock, Executor executor) {
            this.gateway = gateway;
            this.clock = clock;
            this.executor = executor;
        }

        CompletableFuture<SendResult> sendAsync(String userId) {
            return CompletableFuture.supplyAsync(
                () -> gateway.send(userId, Instant.now(clock)),
                executor
            );
        }
    }
}
Why Interviewers Ask This

Interviewers ask this question to see whether I can test work that completes later without creating slow or unreliable tests. They are evaluating whether I choose the correct test boundary, control changing dependencies, wait for completion safely, cover success and failure, prevent background work from leaking, and understand what a unit test can and cannot prove.

Common interview mistakes

Common mistakes include calling Thread.sleep and hoping the work has finished, waiting without a timeout, using the real EmailGateway in a unit test, and checking only that no exception was thrown. Other mistakes include using the real current time, sharing mutable fixtures between tests, verifying private implementation details, ignoring exceptional completion, and depending on test execution order. A test can also leak work when it creates a real executor and never shuts it down. A passing mock based unit test does not prove that the real network contract works, so that boundary needs a separate integration test.

Interview tip

State the test boundary first. Then explain the three controls: mock the gateway, fix the clock, and control the executor. Show that you observe the returned CompletableFuture with a bounded get call, test success and failure separately, and never use Thread.sleep. End by explaining that the unit test checks orchestration while a separate integration test checks the real gateway and scheduling behavior.

Interviewer may ask next
How would you test a retry that waits before calling the EmailGateway again?

I would keep ReminderService as the unit test boundary and replace real time with an injected fake scheduler or controllable clock. The test would configure the first gateway call to fail and the next call to succeed. It would advance test time by the retry delay, assert the final result, and verify the expected number of gateway calls. This matters because Thread.sleep makes the test slow and unreliable. The tradeoff is that the unit test proves retry orchestration but does not prove the real scheduling library.

When would you replace the direct Executor with a real ExecutorService in a test?

I would add a separate integration test when behavior depends on real thread scheduling, executor configuration, cancellation, context propagation, or resource shutdown. The unit test boundary would continue using the direct Executor for fast logic checks. The integration test boundary would include ReminderService and a controlled ExecutorService. It would wait with a bounded future call and shut the executor down afterward. This gives more realistic confidence, but it increases running time and can be harder to diagnose in continuous integration.

128. How do you keep tests fast and reliable in CI?TestingMedium

Question Details

Explain the practices you would use to keep test suites practical in continuous integration.

Short Interview Answer (30-60 seconds)

I keep most pull request feedback in fast deterministic unit tests, then run a smaller set of focused integration tests against real supported infrastructure. For example, I test OrderService with Mockito replacements for OrderRepository and PaymentGateway, plus a fixed Clock. I test the real repository against Postgres with Testcontainers while keeping PaymentGateway as a controlled stub or fake. I reserve broader end to end checks for nightly or pre merge runs. I also isolate workspaces and test data, control time and network access, avoid Thread.sleep, and fix flaky tests instead of hiding them with retries.

Detailed Explanation

The goal is to give developers quick and trustworthy feedback whenever code changes. Most checks should finish quickly and behave the same way on every run. A smaller group should verify that important parts really work together. The slowest checks should cover only important user journeys. This balance prevents the CI pipeline from becoming too slow while still catching problems that simple isolated checks cannot find.

Useful Questions to Ask the Interviewer
  1. How quickly must the pull request pipeline finish?
  2. Which checks must run on every pull request?
  3. Which database and external service boundaries need real integration coverage?
  4. Which broader checks can run nightly or before merge?
  5. Which JDK, build wrapper, test framework, and container setup does the project already use?
How do you keep tests fast and reliable in CI? diagram
How to Explain It in an Interview

I would divide the suite by feedback value and execution cost.

The first layer is the fast suite that runs on every pull request. The system under test is OrderService. OrderRepository, PaymentGateway, and the real system clock are outside the unit test boundary. I replace OrderRepository and PaymentGateway with Mockito mocks and provide a fixed Clock. This removes database, internet, and time based variation. Each test creates small local data, calls a public OrderService method, and checks visible behavior. I verify one important interaction only when that interaction is part of the required behavior. These tests should finish in seconds or a few minutes and may run in parallel when they share no mutable state.

The second layer contains focused integration checks. I start the Spring Boot application with the real OrderRepository and a real Postgres instance supplied by Testcontainers. The application calls OrderRepository, and OrderRepository calls Postgres. The application also calls a controlled PaymentGateway stub or fake directly, with no internet access. This layer verifies mappings, queries, migrations, constraints, and selected component collaboration that Mockito cannot prove. I keep this group small, create deterministic fixture data, and reset database state after every test by rollback, truncation, or recreation according to the project design.

The third layer contains a few broader end to end checks. These cover only critical user paths. They run nightly or before merge instead of on every small commit. They should not test every possible permutation because they are slower, more expensive, and harder to diagnose.

The CI job should use the project Maven or Gradle wrapper inside one clean and repeatable environment. It should use the declared JDK and container image, cache dependencies safely, and provide an isolated workspace. Tests must not depend on execution order, local machine state, uncontrolled random values, current time, production data, or real internet access.

For reliability, I avoid Thread.sleep and wait for observable conditions with a clear timeout. I avoid shared mutable fixtures because one test can change data used by another test. I quarantine a flaky test only as a temporary step while correcting its root cause. I do not use blanket retries to make an unstable suite appear healthy.

The main tradeoff is confidence versus speed. Mockito unit tests are fast and precise, but they cannot prove that SQL, mappings, migrations, transactions, or real external contracts work. Focused integration tests provide stronger confidence but require more setup and execution time. A practical CI suite therefore uses many fast unit tests, fewer real integration checks, and only a small number of broad end to end tests.

Technical Approach
  1. Define the behavior that must be protected.
  2. Put most pull request checks at the OrderService unit boundary.
  3. Replace OrderRepository and PaymentGateway with Mockito mocks.
  4. Provide a fixed Clock so time is deterministic.
  5. Create small independent test data for each test.
  6. Call the public OrderService method.
  7. Assert the visible result and verify only an important contract interaction.
  8. Add focused Spring Boot integration checks with the real OrderRepository and Postgres through Testcontainers.
  9. Keep PaymentGateway as a controlled stub or fake during those integration checks.
  10. Reset database state after each integration test.
  11. Run only critical end to end flows nightly or before merge.
  12. Use the project build wrapper in a clean isolated CI environment.
  13. Remove flaky causes instead of hiding them with sleep calls or blanket retries.
Practical Insights

Traditional algorithmic complexity does not meaningfully describe this strategy. The important costs are test runtime, environment startup, fixture creation, database reset work, CI resources, and maintenance effort. Mockito unit tests are inexpensive because they stay inside one process and avoid real network and database work. Testcontainers checks cost more because Postgres must start, migrations may run, and data must be created and reset. End to end checks cost the most because more deployed or production like components participate. The practical goal is to pay the higher cost only where it adds confidence that faster tests cannot provide.

Why Interviewers Ask This

Interviewers ask this question to evaluate whether a candidate can balance fast feedback, realistic confidence, and maintenance cost. They want to see correct test level selection, clear dependency boundaries, deterministic setup, isolated state, useful failure diagnosis, and practical CI judgment. They also expect the candidate to understand that mocked tests are fast but cannot prove that real database mappings, queries, migrations, or external integrations work.

Common interview mistakes

A common mistake is running every test level on every commit, which makes feedback too slow. Another is over mocking until the test only confirms its own mock setup. A mocked OrderRepository does not test SQL, mappings, constraints, migrations, or transactions. Tests also become unreliable when they share mutable fixtures, depend on execution order, use the current clock, call the real internet, reuse dirty database state, or connect to production data. Thread.sleep creates timing based failures. Blanket retries may hide real defects. Large end to end tests are difficult to maintain when they cover many unrelated behaviors. High coverage also does not prove that assertions are meaningful.

Interview tip

Explain the strategy as a deliberate balance. Start with the OrderService unit boundary and fast pull request feedback. Then explain why the real OrderRepository and Postgres appear only in focused integration checks. Finish with the limitation that mocks cannot prove real integration behavior and the reason broader end to end checks run less often.

Interviewer may ask next
What would you do if a Postgres integration test passes locally but fails randomly in CI?

I would treat it as an isolation, cleanup, or timing problem at the focused integration boundary. I would check whether tests share database rows, schemas, ports, files, environment values, or application state. Each test should create deterministic data and reset Postgres through rollback, truncation, or recreation. I would replace Thread.sleep with a bounded wait for an observable result. I would not add blanket retries because they can hide the actual defect. Stronger isolation may add setup time, but it makes failures repeatable and easier to diagnose.

When should a test move from the pull request suite to the nightly suite?

I would move it when it crosses a broad end to end boundary, takes significant time, or repeats confidence already supplied by faster unit and focused integration checks. Important OrderService rules should remain in the pull request suite through Mockito unit tests and selected Postgres integration checks. The nightly suite should contain only a few complete critical user paths. The tradeoff is that failures in those broad paths may be discovered later, so the pull request suite must still protect the important business rules and integration boundaries.

129. What is the difference between JUnit 4 and JUnit 5?TestingMedium

Question Details

Compare the major API and architecture differences between JUnit 4 and JUnit 5.

Short Interview Answer (30-60 seconds)

For new Java tests, I would use JUnit Jupiter on the JUnit Platform. JUnit 4 is mainly a single library under org.junit. JUnit 5 is modular and separates the Platform, the Jupiter programming model and test engine, and optional Vintage support for older tests. JUnit 5 also changes lifecycle annotations, replaces runners and Rules with a composable extension model, moves exception and timeout checks into assertions, and adds parameterized tests, nested tests, and display names. Vintage helps old tests run, but it does not give them Jupiter features.

Detailed Explanation

This question asks you to explain how an older Java testing tool differs from its newer design. The key idea is that the newer version separates the part that finds and runs tests from the part developers use to write them. It also provides clearer setup names, more flexible reusable behavior, better ways to check errors and time limits, and useful features for testing many inputs. A complete answer should also explain how a project can keep older tests while gradually writing new tests in the newer style.

Useful Questions to Ask the Interviewer
  1. Does the project still contain JUnit 3 or JUnit 4 tests?
  2. Is the goal to compare features, plan a migration, or both?
  3. Does the current suite use custom runners or Rules?
What is the difference between JUnit 4 and JUnit 5? diagram
How to Explain It in an Interview

JUnit 4 is mainly delivered through the junit:junit artifact and uses the org.junit package. Common lifecycle annotations are @Before, @After, @BeforeClass, and @AfterClass. It uses @Ignore to disable a test and @Category to group tests. Extra behavior is commonly added through @RunWith, @Rule, and @ClassRule. A test class can use only one @RunWith runner.

JUnit 5 introduces a modular architecture. The JUnit Platform discovers tests and launches test engines. Jupiter provides the programming model and test engine used for new JUnit 5 tests. Vintage is an optional compatibility engine that runs older JUnit 3 and JUnit 4 tests on the JUnit Platform.

The common annotation mappings are direct. @Before becomes @BeforeEach. @After becomes @AfterEach. @BeforeClass becomes @BeforeAll. @AfterClass becomes @AfterAll. @Ignore becomes @Disabled. @Category is commonly replaced by @Tag. Jupiter APIs are normally imported from org.junit.jupiter.api.

JUnit 5 replaces the JUnit 4 runner and Rule model with an extension model. A test can register an extension with @ExtendWith(MyExtension.class). Multiple extensions can be combined, which is more flexible than the one runner limit in JUnit 4.

JUnit 4 can declare an expected exception or timeout inside @Test, such as @Test(expected = IOException.class) and @Test(timeout = 1000). In Jupiter, these checks are expressed with assertions. assertThrows(IOException.class, executable) checks the expected exception. assertTimeout(Duration.ofSeconds(1), executable) checks that the executable finishes within the given duration. assertAll("calculator", assertion1, assertion2) groups assertions so all failures can be reported together.

JUnit 5 also provides modern features shown in the diagram. @ParameterizedTest can run one test with values supplied by @ValueSource or @CsvSource. @Nested organizes related test classes. @DisplayName gives a test a readable name.

The practical migration choice is to use Jupiter for new tests. Add Vintage only when older JUnit 3 or JUnit 4 tests must keep running during migration. Vintage provides compatibility only. It does not give legacy tests Jupiter features.

Technical Approach

1. Start with the architecture. Explain that JUnit 4 is mainly one library, while JUnit 5 separates the Platform, Jupiter, and optional Vintage engine. 2. Map the main lifecycle and grouping annotations from JUnit 4 to Jupiter. 3. Compare the extension models. Explain the one runner limit in JUnit 4 and the composable extensions in JUnit 5. 4. Compare exception, timeout, and grouped assertion support. 5. Mention parameterized tests, nested tests, and display names. 6. End with the migration decision. Use Jupiter for new tests and Vintage only for legacy compatibility.

Practical Insights

Algorithmic time and space complexity do not apply to this comparison. The main cost is migration and maintenance. Simple annotation changes are usually small. Replacing custom runners or complex Rules can take more work because their behavior must be recreated with Jupiter extensions. Keeping Vintage reduces immediate migration effort, but it adds another engine and allows two test styles to remain in the project. Test execution time depends mainly on the tests themselves, not on the annotation names.

Why Interviewers Ask This

Interviewers ask this question to check whether a Java developer understands the architecture and API changes between JUnit 4 and JUnit 5. They want to see whether the candidate can explain the JUnit Platform, Jupiter, Vintage, lifecycle annotations, assertions, extensions, modern test features, and a safe migration path for older tests.

Common interview mistakes

Common mistakes include saying that JUnit 5 is only a renamed JUnit 4 API, confusing the JUnit Platform with Jupiter, and treating Vintage as the preferred engine for new tests. Another mistake is saying that @RunWith has one identical replacement without explaining the extension model. Candidates may forget that @BeforeClass and @AfterClass become @BeforeAll and @AfterAll, that @Ignore becomes @Disabled, or that @Category is commonly replaced by @Tag. It is also incorrect to claim that Vintage gives old tests Jupiter features. Mixing org.junit and org.junit.jupiter.api imports without a clear migration reason can also create confusing tests.

Interview tip

Start with the architecture, then map the main annotations, explain extensions and assertions, mention the modern features, and finish with the migration decision. Use Jupiter for new tests and Vintage only when legacy tests must still run.

Interviewer may ask next
How would you migrate JUnit 4 tests that depend on custom runners and Rules?

I would keep those tests running through Vintage while migrating them in small groups. The exact boundary is each test class and the behavior supplied by its runner or Rule. I would identify whether that behavior handles setup, cleanup, dependency injection, temporary resources, or another concern. I would then replace it with one or more Jupiter extensions and convert the lifecycle annotations and assertions. This matters because changing annotation names alone may remove required behavior. The tradeoff is that gradual migration is safer, but supporting both engines and both styles increases temporary maintenance.

Should a project keep the Vintage engine after all legacy tests are migrated?

No. Once no JUnit 3 or JUnit 4 tests remain, the Vintage boundary is no longer needed. Removing it reduces test dependencies, simplifies configuration, and leaves the project with one consistent Jupiter model. The tradeoff is that removing it too early can stop unmigrated tests from running, so the team should first confirm through the project test command and continuous integration that the full legacy suite has been converted.

130. Tell me about a time you had to learn a new Java technology quickly.BehavioralMedium

Question Details

Describe what you needed to learn, how you learned it, and how you applied it.

Interview tip:

Use STAR to structure your answer: briefly explain the Situation and Task, make Action the most detailed part, and finish with the Result. For example, describe a project where you had to learn a new Java framework under time pressure, how you identified the essential concepts, practiced them in a safe environment, asked focused questions, applied the technology to the real task, and verified that the solution was reliable.

Situation

In my last role, my team needed to add event driven processing to an existing Java service. The design used Apache Kafka, but I had not worked with Kafka in a production application before. The delivery date was close, so I needed to become productive quickly without creating risk for the service.

Task

I was responsible for building the Java consumer that received events, validated them, and passed valid data to the existing business logic. I also needed to make sure the consumer could handle retries, duplicate messages, and temporary failures safely.

Action

I first limited the learning scope to the features required for our use case. I studied the official Kafka documentation and the examples for the Java client. I focused on consumer groups, offsets, message ordering, retries, and delivery behavior because those concepts directly affected correctness. I then created a small local Java application that produced and consumed test messages. This helped me understand the behavior before I changed the real service. I reviewed the proposed design with a teammate who had Kafka experience and asked specific questions about offset commits and failure handling. Based on that discussion, I used manual offset commits so the service would only confirm a message after processing completed successfully. I also made the processing idempotent, which means that handling the same message more than once would not create duplicate results. I added tests for valid messages, invalid messages, repeated messages, and temporary downstream failures. During implementation, I shared what I had learned with the team and documented the key configuration choices so others could support the component.

Result

I completed the consumer in time and integrated it safely with the existing Java service. The tests gave the team confidence that retries and duplicate delivery would not corrupt data. I also became comfortable enough with Kafka to review related changes and help teammates with the same concepts. I learned that when time is limited, I can learn faster by focusing on the exact production risks, building a small working example, and validating important decisions with someone who has deeper experience.

Why Interviewers Ask This

Interviewers ask this question to understand how quickly a candidate can learn an unfamiliar technology while still protecting code quality and delivery commitments. A strong answer shows focused learning, practical experimentation, good judgment about risk, effective use of documentation and teammates, and the ability to apply new knowledge to a real Java problem.

Interviewer may ask next
Why did you choose manual offset commits for the Kafka consumer?

I chose manual offset commits because I wanted the service to confirm a message only after the business operation completed successfully. This reduced the chance of losing a message when processing failed after it was received. I combined that choice with idempotent processing so a retried message could be handled safely.

What would you do differently if you had more time to learn Kafka?

I would spend more time testing operational behavior such as consumer rebalancing, slow processing, and monitoring in an environment closer to production. I would also create a shared example project for the team so future developers could learn the same patterns more quickly.

More questions load as you scroll

Disclaimer: This interview guide is for educational and informational purposes only. It is designed to help readers prepare, but it does not guarantee any interview result, hiring decision, offer, or outcome. Interview questions, hiring criteria, and preferred answers can vary by employer, interviewer, industry, location, and time. The examples and explanations reflect the authors' research and judgment, are provided without warranties of any kind, and should not be treated as the only correct approach. Diagrams are simplified illustrations intended to highlight the main components and their interactions; actual systems and implementations may be more complex. Alternative approaches may be equally valid or better suited to a particular question, context, or interviewer. To the fullest extent permitted by applicable law, the author, contributors, and publisher are not liable for decisions made, actions taken, or losses incurred based on this guide.