Test-driven development (TDD) means writing a failing test before the code that makes it pass, then cleaning up: red, green, refactor. It's a developer practice, but SDETs are expected to understand it, pair on it and explain how it changes a team's test strategy. This guide explains TDD through the interview questions asked at each experience level. For how it differs from BDD, see BDD vs TDD.

Fresher Level (0–6 Months)

What is TDD? Red-Green-Refactor Cycle

TDD (Test-Driven Development) is a software development practice where you write a failing test first, then write minimum code to pass the test, and finally refactor the code. The cycle is:

  • RED: Write a failing test
  • GREEN: Write minimum code to pass the test
  • REFACTOR: Clean up the code while keeping tests green

What is TDD and why is it used?

TDD stands for Test-Driven Development. It is a development approach where tests are written before the actual production code. The key benefits are: (1) It forces you to think about design before coding, (2) It keeps test coverage high, because no production code is written without a test, (3) It leads to simpler, more focused code since you only write what is needed to pass tests, (4) It acts as living documentation, and (5) It reduces debugging time because failures are caught immediately.

Explain the Red-Green-Refactor cycle with an example.

Red-Green-Refactor is the core cycle of TDD:

  • RED: Write a test that fails. Example: write a test testAdd() that calls Calculator.add(2, 3) and expects 5. This fails because Calculator class doesn't exist yet.
  • GREEN: Write the minimum code to make the test pass. Create a Calculator class with an add() method that returns a + b.
  • REFACTOR: Clean up the code. Maybe extract constants, improve variable names, remove duplication — all while keeping the test green.

This cycle repeats for every new piece of functionality.

What is the difference between TDD and traditional testing?

In traditional testing, code is written first, then tests are written after to verify the code works. In TDD, tests are written first before any production code. TDD advantages: better design since testability is considered upfront, no code is written without a failing test driving it, and code coverage is naturally high. Traditional testing often leads to tests being skipped under deadline pressure and lower overall coverage.

What tools do you use for TDD in Java?

For Java TDD: JUnit 5 is the most popular testing framework. Mockito is used for mocking dependencies. AssertJ provides fluent assertion APIs. Maven or Gradle manage build lifecycle and run tests. An IDE like IntelliJ IDEA or Eclipse provides real-time test feedback. A typical stack: JUnit 5 + Mockito + AssertJ in a Maven project.

What is an assertion in TDD?

An assertion is a statement in a test that verifies the expected outcome matches the actual result. If the assertion fails, the test fails. Example in JUnit: assertEquals(5, calculator.add(2,3)) asserts that adding 2 and 3 gives 5. Common assertions: assertEquals, assertTrue, assertFalse, assertNull, assertNotNull, assertThrows (for exceptions).

Writing Failing Tests First & Test Naming Conventions

Good test naming is critical. Follow the pattern: methodName_StateUnderTest_ExpectedBehavior or Given_When_Then format.

Example: add_TwoPositiveNumbers_ReturnsSum()

Example: should_ThrowException_When_DivideByZero()

Why do we write a failing test first in TDD?

Writing a failing test first ensures: (1) The test actually tests something — if you write passing tests, they might be testing nothing, (2) It forces you to think about the interface/API before implementation, (3) It confirms the test framework is running correctly, (4) It prevents false positives where tests pass trivially. A green test without a prior red state gives you no confidence.

What is a good test naming convention? Give examples.

Common naming patterns:

  • methodName_StateUnderTest_ExpectedBehavior: add_NegativeNumbers_ReturnsNegativeSum()
  • Given_When_Then: givenValidUser_WhenLogin_ThenTokenIsReturned()
  • should_ExpectedBehavior_When_StateUnderTest: should_ReturnZero_When_InputIsEmpty()

Good names act as documentation. Avoid vague names like test1() or testAdd() — they reveal nothing about what scenario is being tested.

What is test isolation and why is it important?

Test isolation means each test is independent and does not rely on other tests or shared state. Isolated tests: (1) Can run in any order, (2) Don't fail due to side effects from other tests, (3) Are easier to debug since failures are localized. Achieved by using @BeforeEach/@AfterEach to set up and tear down state, not sharing mutable static variables, and using mocks to isolate from external dependencies.

Unit Testing Basics with JUnit / TestNG

Unit testing tests the smallest testable unit of code (a method/class) in isolation. Key annotations in JUnit 5: @Test, @BeforeEach, @AfterEach, @BeforeAll, @AfterAll, @Disabled, @DisplayName, @ParameterizedTest

What is the difference between JUnit 4 and JUnit 5?

JUnit 5 (Jupiter) is a major rewrite:

  • Architecture: JUnit 5 has 3 modules (Platform, Jupiter, Vintage); JUnit 4 is monolithic.
  • Annotations: @RunWith → @ExtendWith, @Before/@After → @BeforeEach/@AfterEach, @BeforeClass → @BeforeAll.
  • Java version: JUnit 5 requires Java 8+, JUnit 4 supports Java 5+.
  • Parameterized tests: JUnit 5 has built-in @ParameterizedTest with @ValueSource, @CsvSource; JUnit 4 needed @RunWith(Parameterized.class).
  • Extensions: JUnit 5 uses flexible Extension model; JUnit 4 used Rules.

What is the difference between JUnit and TestNG?

Both are Java testing frameworks. Key differences:

  • Dependencies: JUnit has no dependency XML; TestNG uses testng.xml for suite configuration.
  • Parallel testing: TestNG has built-in parallel execution support; JUnit needs plugins.
  • Data providers: TestNG has @DataProvider; JUnit uses @ParameterizedTest.
  • Groups: TestNG supports test grouping with groups attribute; JUnit uses tags.
  • Listeners: TestNG has ITestListener; JUnit uses @ExtendWith.

For large Selenium frameworks, TestNG is preferred due to its better suite management and parallel execution.

What is @BeforeEach vs @BeforeAll in JUnit 5?

@BeforeEach runs before EVERY test method in the class — used for setup that needs to be fresh per test (e.g., creating a new instance of the class under test). @BeforeAll runs ONCE before all tests in the class — used for expensive setup like starting a database or server. @BeforeAll methods must be static (unless @TestInstance(Lifecycle.PER_CLASS) is used).

Advertisement

Junior Level (6 Months – 1.5 Years)

Mocking and Stubbing with Mockito

Mocking replaces real dependencies with controlled fakes.

  • Mock: A fake object that records interactions and can verify them.
  • Stub: A fake that returns predefined values.
  • Spy: A partial mock that delegates to real methods unless overridden.

Key Mockito annotations: @Mock, @Spy, @InjectMocks, @Captor

What is the difference between a Mock, Stub, Spy, and Fake?

  • Mock: A test double that records all interactions. You verify behavior afterwards (e.g., verify(mockService).save(user)).
  • Stub: Returns a predefined value when called. Used for state verification (e.g., when(repo.findById(1)).thenReturn(user)).
  • Spy: Wraps a real object. Real methods are called unless overridden. Useful when you want to test real logic but stub some methods.
  • Fake: A working implementation meant for testing (e.g., in-memory database instead of real DB).
  • Dummy: Passed as a required parameter but never actually used in the test.

How do you create a mock with Mockito and use it?

Three ways to create mocks:

1. @Mock annotation + @ExtendWith(MockitoExtension.class) — most common.

2. Mockito.mock(MyClass.class) — programmatic creation.

3. @Spy for partial mocking of real objects.

Example:

@Mock UserRepository userRepo;
@InjectMocks UserService userService;
@Test void testFindUser() {
when(userRepo.findById(1L)).thenReturn(new User(1L, "Alice"));
User result = userService.getUser(1L);
assertEquals("Alice", result.getName());
verify(userRepo, times(1)).findById(1L);
}

What is @InjectMocks in Mockito?

@InjectMocks creates an instance of the class under test and automatically injects all @Mock and @Spy fields into it via constructor injection, setter injection, or field injection (in that order). This eliminates manual wiring. Example: if UserService has a UserRepository dependency, marking UserService with @InjectMocks and UserRepository with @Mock will automatically wire them together.

How do you test that an exception is thrown in JUnit 5?

Use assertThrows():

@Test void should_ThrowException_When_DivideByZero() {
Calculator calc = new Calculator();
ArithmeticException ex = assertThrows(ArithmeticException.class, () -> calc.divide(10, 0));
assertEquals("Cannot divide by zero", ex.getMessage());
}

This replaces JUnit 4's @Test(expected = Exception.class) which couldn't inspect the exception message.

What is code coverage and what are the different types?

Code coverage measures what percentage of production code is executed by tests.

  • Line/Statement coverage: % of lines executed.
  • Branch coverage: % of decision branches (if/else, switch) exercised — more meaningful than line coverage.
  • Method coverage: % of methods called.
  • Condition coverage: % of boolean sub-expressions evaluated to true and false.

Tools: JaCoCo for Java, Istanbul for JavaScript. 80%+ branch coverage is a common target. 100% coverage doesn't mean bug-free — it means every line ran, not every scenario was tested.

Test Pyramid & Integration Testing

The Test Pyramid (Mike Cohn) has 3 layers:

  • Unit Tests (base): Fast, isolated, many
  • Integration Tests (middle): Test component interactions
  • E2E / UI Tests (top): Slow, brittle, few

Balance: 70% unit, 20% integration, 10% E2E

What is the Test Pyramid and why does it matter?

The Test Pyramid (by Mike Cohn) guides the distribution of test types:

  • Base — Unit Tests: Fast (<1ms), cheap, isolated, many (70%). Test individual methods.
  • Middle — Integration Tests: Moderate speed, test component integration (DB, APIs), fewer (20%).
  • Top — E2E/UI Tests: Slow (seconds/minutes), expensive, brittle, very few (10%).

Violating the pyramid leads to an 'Ice Cream Cone' anti-pattern — mostly slow, flaky E2E tests and no fast unit tests, leading to slow CI pipelines and poor developer feedback.

What is the difference between unit testing and integration testing?

Unit testing tests a single class/method in complete isolation using mocks for all dependencies. Integration testing tests how multiple components work together with real (or near-real) dependencies — e.g., testing a Service class with a real database or a real HTTP client. Integration tests are slower but catch integration bugs (mismatched contracts, wrong SQL queries) that unit tests miss.

Mid-Level (1.5 – 3 Years)

Mutation Testing

Mutation testing verifies test quality by introducing small bugs (mutations) into production code and checking if tests catch them.

  • Mutant killed = test caught the bug (good)
  • Mutant survived = test missed the bug (bad)

Tools: PIT (Java), Stryker (JS/TS)

What is mutation testing and why is it more valuable than code coverage?

Mutation testing automatically introduces bugs (mutants) into your code — changing + to -, > to >=, removing return statements, etc. — and then runs your tests. If a test fails, the mutant is 'killed' (good). If all tests pass with the mutant, the mutant 'survived' — meaning your tests missed a real bug scenario.

This is more valuable than code coverage because 100% coverage can be achieved with assertions-free tests. Mutation score (killed/total mutants) measures actual test effectiveness. Tools: PIT for Java (mvn org.pitest:pitest-maven:mutationCoverage).

What is Outside-In TDD vs Inside-Out TDD?

Outside-In TDD (London School): Start from high-level acceptance/integration tests, then drive implementation downward using mocks. Focuses on interfaces and collaboration between objects. Good for when you know the API but not implementation.

Inside-Out TDD (Chicago/Detroit School): Start from the lowest-level units, build upward. No mocking at unit level — test real collaborators. Design emerges from implementation. Good when exploring domain logic.

In practice, most teams use a mix: outside-in for new features to define APIs, inside-out for complex algorithms.

Contract Testing

Contract testing ensures microservices honor the contracts expected by their consumers.

  • Consumer: defines what it expects
  • Provider: verifies it satisfies those expectations

Tool: Pact (pact.io)

What is Consumer-Driven Contract Testing and why use it?

In microservices, when Service A calls Service B, a contract defines what A expects from B. Consumer-Driven Contract Testing (CDCT) means the consumer (A) writes the contract (expectations) and the provider (B) verifies it can satisfy them.

Tool: Pact. Process: Consumer writes Pact tests defining expected request/response, Pact generates a contract file (pact file), Provider runs a verification against this pact file in CI. Benefits: catches breaking API changes without full integration environments, enables independent deployment of services.

Senior Level (3 – 5 Years)

TDD in Microservices & Test Architecture Decisions

At senior level, TDD involves architectural decisions:

  • Choosing the right test strategy per service
  • Contract testing between services
  • Testing event-driven (Kafka) flows
  • Hexagonal Architecture enabling testability

How do you apply TDD in a microservices architecture?

TDD in microservices requires a layered strategy:

1. Unit tests per service — test domain logic in isolation, heavy mocking.

2. Integration tests per service — test persistence layer, real DB (e.g., TestContainers).

3. Contract tests between services — Pact to verify API contracts without needing all services running.

4. Component tests — test the service as a black box via its API, stubbing external dependencies with WireMock.

5. E2E tests — minimal, only for critical paths.

The key challenge: avoiding shared test environments. Each service should be testable independently.

What is Hexagonal Architecture and how does it improve testability?

Hexagonal Architecture (Ports & Adapters, by Alistair Cockburn) separates business logic from infrastructure. The domain core has no dependencies on frameworks, databases, or external systems. Ports are interfaces the core exposes or requires. Adapters are implementations (REST controller, JPA repository, Kafka producer).

Testability benefit: The domain core can be tested with pure unit tests using no mocks of frameworks. Adapters are tested with integration tests. Switching a real DB adapter to an in-memory adapter for tests is trivial. This makes TDD extremely natural since the core is plain Java/Python/etc.

How do you lead TDD adoption in a team that has never used it?

Gradual adoption strategy:

1. Start with new code — mandate TDD for new features only, not legacy code.

2. Run TDD workshops — use katas like FizzBuzz, Roman Numerals to build muscle memory.

3. Pair programming — pair TDD-experienced devs with those learning.

4. Define DoD — add 'unit tests written with TDD approach' to Definition of Done.

5. Show metrics — present code coverage trends, mutation scores, and defect rates over sprints to demonstrate value.

6. Tooling — set up JaCoCo/SonarQube to report coverage in CI, fail build below threshold.

7. Legacy code — use Characterization Tests (Michael Feathers' approach) to add tests before refactoring legacy code.

How do you test asynchronous/event-driven code with TDD?

Async testing challenges: results aren't immediate. Strategies:

1. Synchronous domain logic — design event handlers to call synchronous domain services that can be unit-tested purely.

2. Awaitility library (Java) — poll for conditions: await().atMost(5, SECONDS).until(() -> result.isDone()).

3. Embedded Kafka (spring-kafka-test) — spin up a real Kafka broker in tests.

4. TestContainers — run Kafka in Docker for integration tests.

5. CompletableFuture / Reactive testing — use StepVerifier (Project Reactor) for reactive streams testing.

TDD in One Example

// RED: the test fails because PriceCalculator doesn't exist yet
@Test
void discount_OrderAbove1000_Gets10Percent() {
    PriceCalculator calc = new PriceCalculator();
    assertEquals(1350.0, calc.finalPrice(1500.0), 0.001);
}

// GREEN: the simplest code that passes
class PriceCalculator {
    double finalPrice(double total) {
        return total > 1000 ? total * 0.9 : total;
    }
}

// REFACTOR: name the rule, add the next failing test (exactly 1000? negative totals?), repeat
class PriceCalculator {
    private static final double THRESHOLD = 1000;
    private static final double DISCOUNT = 0.10;
    double finalPrice(double total) {
        if (total < 0) throw new IllegalArgumentException("total must be positive");
        return total > THRESHOLD ? total * (1 - DISCOUNT) : total;
    }
}

FAQs

Is TDD the same as writing unit tests?

No. Unit tests can be written after the code; TDD writes them first and uses them to drive the design, one small failing test at a time.

Does TDD guarantee 100% code coverage?

It tends to produce high coverage because no code is written without a failing test, but coverage alone doesn't prove the tests check the right behaviour; mutation testing helps measure that.

Do SDETs practise TDD?

Often for framework code and utilities, and they support developers practising it: reviewing tests, improving test design and keeping the overall test pyramid balanced.