Top 64 Software Testing / TDD Interview Questions & Answers (2026)
About Software Testing / TDD
Top 100 Software Testing and TDD interview questions covering unit testing, integration testing, test-driven development, mocking, CI/CD testing, performance testing, and quality assurance. Companies hiring for Software Testing / TDD roles test this knowledge at every stage — from first-round screens that check the basics to final-round conversations about real-world trade-offs.
What to Expect in a Software Testing / TDD Interview
Expect a mix of conceptual and practical Software Testing / TDD questions: clear definitions and core-concept checks for junior roles, hands-on scenario questions for mid-level roles, and architecture or trade-off discussions for senior roles. Interviewers usually move from foundational topics toward the kind of problems you'd actually face on the job.
How to Use This Guide
Work through the Software Testing / TDD questions in order — Beginner, then Intermediate, then Advanced — so each concept builds on the last. Every question has its own page; bookmark the ones that trip you up and revisit them the day before your interview.
Curated by Tech Baithak Editorial Team · Last updated: June 2026
Core concepts every Software Testing / TDD developer must know.
01
What is software testing?
Software testing is the process of evaluating a software system to identify gaps, errors, or missing requirements compared to the actual requirements. It involves executing software with the intent of finding defects and verifying that the software does what it is supposed to do. Testing serves multiple purposes: (1) Defect detection: find bugs before they reach users. (2) Quality assurance: verify the software meets requirements. (3) Reliability: ensure the system behaves correctly under various conditions. (4) Documentation: tests serve as living documentation of expected behavior. Testing can be manual or automated, functional or non-functional, and is a critical part of the software development lifecycle. The earlier defects are found, the cheaper they are to fix — a bug in production can cost 100× more to fix than one found in unit testing.
02
What is the difference between verification and validation in testing?
Verification asks: "Are we building the product right?" — it checks that the software conforms to its specification and design documents. It involves reviews, walkthroughs, inspections, and static analysis — checking the product against requirements without necessarily running it. Validation asks: "Are we building the right product?" — it checks that the software meets the actual needs of the user/stakeholder. It involves dynamic testing (running the software) and user acceptance testing. Barry Boehm's classic distinction: verification = does it meet the spec? validation = does it meet the user's need? Example: a program might be verified (it does exactly what the specification says) but not validated (the specification was wrong about what the user needed). Both are essential — verification catches implementation errors; validation catches requirements errors.
03
What is unit testing?
Unit testing involves testing individual units (functions, methods, classes) of code in isolation to verify they behave correctly. "Unit" is the smallest testable piece of code. Key characteristics: Isolated: dependencies are replaced with test doubles (mocks/stubs). Fast: run in milliseconds. Deterministic: same input always produces same result. Independent: no dependency between tests. Unit tests are the foundation of the testing pyramid — the most numerous, fastest, and cheapest to write. Example in JavaScript (Jest): test("add returns sum", () => { expect(add(2, 3)).toBe(5); });. Benefits: (1) Fast feedback during development. (2) Regression detection. (3) Forces good design (testable code is often well-designed). (4) Documentation of intended behavior. (5) Enables safe refactoring. Frameworks: JUnit (Java), NUnit/xUnit (C#), pytest (Python), Jest/Vitest (JavaScript), PHPUnit (PHP).
04
What is integration testing?
Integration testing verifies that multiple units/components work correctly together when integrated. While unit tests test in isolation (mocking dependencies), integration tests use real implementations of dependencies — real databases, real APIs, real file systems. Types: Big Bang: integrate all at once (hard to isolate failures). Top-down: test from high-level components down, stub lower levels. Bottom-up: test from low-level components up, real lower-level but stub higher. Incremental: integrate and test component by component. Examples: testing that a service correctly stores data in a real database, testing that two microservices communicate correctly over HTTP. Integration tests are slower than unit tests (due to real dependencies), harder to set up, and sit in the middle of the testing pyramid. Tools: Testcontainers (spin up Docker containers for DB/services), WireMock (HTTP mocking), in-memory databases.
05
What is end-to-end (E2E) testing?
End-to-end (E2E) testing tests the entire application flow from the user interface to the database, verifying that the complete system works as expected from a user's perspective. It simulates real user scenarios through the full stack: browser → frontend → backend → database and back. Examples: a user can sign up, log in, create a product, checkout, and receive an email — all tested as one complete flow. Tools: Playwright (Microsoft — fast, cross-browser, highly recommended), Cypress (JavaScript-centric, good DX), Selenium (older, widely supported), Puppeteer (Chrome/Chromium). Characteristics: slow (seconds to minutes per test), brittle (UI changes break tests), expensive to maintain, but high confidence. At the top of the testing pyramid — fewer E2E tests, more unit tests. Best practice: use E2E for critical user journeys only (login, checkout, core workflows).
06
What is the testing pyramid?
The testing pyramid (coined by Mike Cohn) is a model for organizing tests into layers based on speed, cost, and coverage: Base (Unit tests): the widest layer — fast, cheap, isolated, numerous. Should be the majority of your tests (70%). Middle (Integration tests): slower, test component interactions, fewer than unit tests (20%). Top (E2E/UI tests): slowest, most expensive, fewest — only critical scenarios (10%). The pyramid shape indicates the ideal ratio: many fast unit tests at the bottom, fewer integration tests in the middle, very few E2E tests at the top. Anti-pattern — Ice cream cone: mostly E2E tests and manual testing at top, few unit tests — inverted pyramid, expensive and fragile. Trophy model (Kent C. Dodds): emphasizes integration tests as the sweet spot, with unit tests for pure functions and few E2E tests. Regardless of shape, the principle holds: test at the lowest possible level for each concern.
07
What is Test-Driven Development (TDD)?
Test-Driven Development (TDD) is a software development practice where tests are written before the production code. The cycle: Red: write a failing test that defines the desired behavior. Green: write the minimum code to make the test pass. Refactor: clean up the code while keeping tests green. Repeat. Benefits: (1) Forces good design: code written for testability tends to be more modular and decoupled. (2) Documentation: tests document expected behavior. (3) Regression safety: comprehensive test suite catches regressions. (4) Confidence: refactor safely. (5) Fewer bugs: think through requirements before coding. (6) Feedback: instant feedback on changes. Challenges: learning curve, initial slowdown, tests must be maintained as production code. Not appropriate for all situations (UI prototyping, exploratory work, very uncertain requirements). Created by Kent Beck as part of Extreme Programming.
08
What is BDD (Behavior-Driven Development)?
BDD (Behavior-Driven Development) extends TDD by writing tests in a natural language format that business stakeholders can understand — bridging communication between developers, testers, and business. Tests are written as scenarios in Gherkin language: Given (initial context), When (action), Then (expected outcome). Example: Feature: User Login Scenario: Successful login Given the user is on the login page When they enter valid credentials And click Login Then they should be redirected to the dashboard. Tools: Cucumber (Java/Ruby/JavaScript), SpecFlow (C#), Behave (Python), Behat (PHP). Step definitions connect Gherkin to actual test code. BDD promotes shared understanding of requirements, living documentation, and tests that reflect business value. It's particularly valuable when business stakeholders are involved in defining acceptance criteria.
09
What is a test double?
A test double is any object used in tests to replace a real dependency. The term (coined by Gerard Meszaros) covers several types: Dummy: passed around but never actually used — just fills parameter lists. Stub: returns hardcoded responses to calls — "when called with X, return Y." No verification of calls. Fake: working implementation but simpler than production (e.g., in-memory database instead of real DB). Mock: pre-programmed with expectations about which calls it should receive — verifies interactions. If expected calls don't happen, the test fails. Spy: records calls and allows verification afterward — like a mock but more flexible (doesn't pre-define expectations). In practice, "mock" is often used loosely to mean any test double. Frameworks: Mockito (Java), Moq/NSubstitute (C#), unittest.mock (Python), Jest mocks, Sinon.js (JavaScript), Mockery/Prophecy (PHP).
10
What is mocking in unit testing?
Mocking is creating controlled test doubles that simulate the behavior of real dependencies, allowing unit tests to run in isolation without actual databases, network calls, or external services. Benefits: (1) Speed — tests run in milliseconds instead of seconds. (2) Reliability — no flaky tests due to network issues or database state. (3) Isolation — test one unit without testing its dependencies simultaneously. (4) Control — simulate edge cases (network timeout, specific error responses) that are hard to reproduce with real services. Example in Jest: jest.mock('./userService'); userService.getUser.mockResolvedValue({ id: 1, name: 'Alice' });. In Moq (C#): var mockRepo = new Mock<IUserRepository>(); mockRepo.Setup(r => r.GetById(1)).Returns(new User { Name = "Alice" });. Rule of thumb: mock things you don't own (external services, databases), use real implementations for things you own (your own services, domain logic).
11
What is code coverage?
Code coverage is a metric measuring what percentage of the source code is executed when the test suite runs. Types: Line/Statement coverage: percentage of lines executed. Branch coverage: percentage of branches (if/else paths) taken. Function coverage: percentage of functions called. Path coverage: percentage of unique paths through the code (can be exponential). Tools: Istanbul/nyc (JavaScript), JaCoCo (Java), coverage.py (Python), Coverlet (.NET), PHPUnit coverage. High coverage is NOT sufficient — it means lines were executed, not that the assertions were correct. 100% coverage doesn't mean bug-free. Common misconception: 80% coverage mandate — this can lead to meaningless tests written purely to hit the number. Better: focus on testing all meaningful behaviors, edge cases, and error paths. Coverage is a useful tool to find untested code (low coverage = risk), not a quality metric on its own.
12
What is the difference between a test stub and a mock?
Both are test doubles but serve different purposes. A stub provides indirect inputs to the system under test — it returns predefined values when called. You don't verify how or whether the stub was called — you only care about the output it provides. Example: stub.GetUser(1) always returns a specific User object regardless of how many times it's called. A mock verifies indirect outputs — it sets expectations on what calls should be made and verifies them at the end of the test. If the expected call doesn't happen (or happens with wrong args), the test fails. Example: mock.Verify(r => r.SaveUser(It.IsAny<User>()), Times.Once);. Stubs answer the question "what data should the dependency return?" Mocks answer the question "was the dependency called correctly?" Over-mocking (verifying every interaction) leads to brittle tests. Prefer stubs when you only care about behavior; use mocks only when the interaction itself (the call) is what you're testing.
13
What is regression testing?
Regression testing verifies that existing functionality continues to work correctly after new code changes (bug fixes, new features, refactoring). The goal is to ensure that "fixing one thing doesn't break another." A regression is a bug that existed, was fixed, but reappeared due to a subsequent change. Regression testing is often automated — the test suite is run on every code change (CI/CD pipeline). Types: Full regression: run all tests. Partial/selective regression: run tests related to changed code. Automated unit, integration, and E2E tests collectively form the regression suite. Without a good test suite, any change carries the risk of undetected regressions — organizations without automated testing often experience a slow, painful "release anxiety" where any change might break something. TDD naturally produces a comprehensive regression suite as a byproduct of development.
14
What is smoke testing?
Smoke testing (also called "sanity testing" or "build verification testing") is a preliminary test to check whether the most critical functionalities of the system work before investing more time in deeper testing. The name comes from electronics — powering on a new circuit board and checking if it smokes (basic validation). In software: a small subset of tests (the most critical happy paths) run quickly to verify the basic functionality works. Example: after a deployment, run smoke tests to verify: can users log in? can the homepage load? does the API return 200? If smoke tests fail, the build is rejected immediately — no point running full regression on a broken build. Smoke tests are typically the first stage of a CI/CD pipeline after deployment. They should be fast (< 5 minutes) and cover only critical paths — not a comprehensive test.
15
What is acceptance testing?
Acceptance testing (also called User Acceptance Testing — UAT) validates that the software meets the business requirements and is acceptable to the end user, customer, or stakeholder. It's the final testing phase before a product goes live. Types: Alpha testing: performed by developers/QA within the organization before external release. Beta testing: performed by a limited group of real users in a production-like environment. Contract acceptance testing: verifies the system meets a formal contract specification. Regulation acceptance testing: verifies compliance with regulations. UAT is typically manual, performed by business users (not developers). In agile, acceptance criteria are defined before development (BDD/Gherkin), and acceptance tests verify those criteria are met. Automated acceptance tests can be part of a CI/CD pipeline. The distinction from integration/system testing: acceptance testing is about business requirements; system testing is about technical requirements.
16
What is functional vs non-functional testing?
Functional testing verifies that the software functions as specified — testing features, user interactions, and business logic. Examples: "Does the login button log the user in?" "Does the payment form correctly process a purchase?" Includes: unit tests, integration tests, acceptance tests, E2E tests. Non-functional testing tests the quality attributes of the software beyond its features. Examples: Performance testing: how fast? how many concurrent users? Load testing: behavior under expected load. Stress testing: behavior under extreme load (breaking point). Security testing: pen testing, vulnerability scanning. Usability testing: is it easy to use? Accessibility testing: WCAG compliance. Compatibility testing: cross-browser, cross-device. Reliability/availability testing: uptime, MTBF. Non-functional requirements are often overlooked but define the quality of a system — a functionally correct but unusably slow app is still a failure.
17
What is a test fixture?
A test fixture is the fixed state of the environment required for a test to execute consistently — setting up the preconditions for the test. It includes: test data (specific records in the database, JSON payloads), initialized objects (services, repositories), configured mocks, and cleaned-up state after the test. Fixture setup/teardown methods: xUnit (C#): constructor (setup) / IDisposable.Dispose() (teardown). NUnit: [SetUp] / [TearDown] per test; [OneTimeSetUp] / [OneTimeTearDown] once per class. JUnit (Java): @BeforeEach / @AfterEach. Jest: beforeEach / afterEach, beforeAll / afterAll. Fixtures should be: minimal (only what the test needs), fast to set up, and isolated (one test's fixture doesn't affect another). Database fixtures: use transactions rolled back after each test, or test containers with fresh state.
18
What is a test suite?
A test suite is a collection of tests grouped together for execution. It can be organized by: feature/module (all tests for the User module), type (all unit tests, all integration tests), or team ownership. Test suites enable: (1) Running a specific subset of tests (fast feedback on a feature). (2) Parallel execution. (3) Targeted CI/CD stages (unit suite → integration suite → E2E suite). Most testing frameworks support grouping via naming conventions, tags/categories, or configuration: JUnit 5: @Tag("slow"). xUnit: trait-based filtering. pytest: markers (@pytest.mark.slow). NUnit: [Category("Integration")]. In CI pipelines, suites are often run in parallel to reduce total time: unit tests run on every commit (fast), integration tests on PR, E2E on main branch before deployment. A well-structured test suite is maintained like production code — poor organization leads to slow, brittle, hard-to-debug test runs.
19
What is the AAA pattern in unit testing?
The AAA (Arrange-Act-Assert) pattern is the standard structure for unit tests, providing clarity and consistency. Arrange: set up the test preconditions — create objects, configure mocks, prepare test data. Act: execute the code being tested — call the method/function under test. Assert: verify the outcome — check return value, verify state change, verify mock interactions. Example: // Arrange var mockRepo = new Mock<IOrderRepo>(); var service = new OrderService(mockRepo.Object); var order = new Order { Amount = 100 }; // Act var result = service.ProcessOrder(order); // Assert Assert.True(result.IsSuccess); mockRepo.Verify(r => r.Save(order), Times.Once);. Benefits: tests are self-documenting, easy to read, and consistently structured. Each test should have exactly one Act and one concept being tested. Also known as Given-When-Then (from BDD): Given (Arrange) → When (Act) → Then (Assert).
20
What is test isolation?
Test isolation means each test runs independently without being affected by or affecting other tests. Violations of isolation: (1) Shared state: a test modifies a global variable or database record that another test depends on. (2) Test order dependency: tests pass only when run in a specific order (Test A must run before Test B). (3) Resource sharing: tests share a file or network socket without proper cleanup. (4) Time dependencies: tests that use DateTime.Now without injection (different results at different times). How to achieve isolation: (1) Reset state: use SetUp/TearDown to restore initial state. (2) Mock dependencies: replace external dependencies with controlled mocks. (3) Use transactions: wrap database tests in a transaction and roll back after each test. (4) Inject time: use IClock interface instead of static DateTime.Now. (5) Parallel safety: ensure tests can run concurrently. Isolated tests are reliable, order-independent, and diagnosable.
21
What is a happy path test vs an edge case test?
A happy path test tests the expected, normal flow — the scenario where everything goes right, inputs are valid, and the system returns the expected successful result. Example: test that a valid user login succeeds. Edge case tests test boundary conditions and unusual inputs that might reveal bugs: (1) Boundary values: minimum/maximum valid values, values just inside/outside boundaries (0, -1, 1, Integer.MAX_VALUE). (2) Empty/null inputs: empty string, null, empty array. (3) Large inputs: very long strings, large arrays. (4) Invalid types: wrong format dates, negative quantities. (5) Concurrent access: multiple simultaneous requests. (6) Failure scenarios: network timeout, database unavailable. Common bugs hide in edge cases. The happy path test is necessary but not sufficient — a comprehensive test suite tests both happy paths and the most important edge cases. The equivalence class partitioning technique identifies representative edge cases systematically.
22
What is a flaky test?
A flaky test is a test that produces inconsistent results — passing sometimes and failing sometimes without any code changes. Flaky tests are a major problem in CI/CD pipelines: they erode trust in the test suite (engineers start ignoring failures), slow down pipelines (forced reruns), and hide real bugs. Common causes: (1) Race conditions: tests with async operations where timing varies. (2) Order dependence: relies on another test having run first. (3) Shared state: global variables or databases not properly reset. (4) Network/external service dependency: external calls with variable latency or availability. (5) Time/date dependence: tests that compare against current time. (6) Random data: tests using random values without fixed seeds. Fixing flaky tests: add proper waits/retries, mock external dependencies, isolate state, use fixed time (inject a clock), use fixed seeds for random. Track and quarantine flaky tests immediately — don't let them accumulate.
23
What is performance testing?
Performance testing evaluates how a system behaves under expected and unexpected load conditions — measuring speed, scalability, and stability. Types: Load testing: testing behavior under expected normal load (e.g., 1,000 concurrent users). Stress testing: testing beyond normal capacity to find the breaking point — what happens under extreme load? Spike testing: sudden large increase in load (e.g., Black Friday traffic surge). Endurance/Soak testing: testing under normal load for an extended period to find memory leaks or resource exhaustion. Volume testing: testing with large amounts of data. Scalability testing: how the system scales with increasing resources. Key metrics: response time (p50, p95, p99 percentiles), throughput (requests per second), error rate, resource utilization (CPU, memory, I/O). Tools: k6, JMeter, Gatling, Locust, Artillery. Performance testing should be part of CI/CD, not an afterthought.
24
What is test automation?
Test automation is using software tools to execute tests automatically, compare actual outcomes to expected outcomes, and report results — replacing or supplementing manual testing. Benefits: (1) Speed: automated tests run in seconds/minutes vs hours for manual. (2) Repeatability: same tests run consistently on every change. (3) Coverage: run more tests than humanly possible manually. (4) Regression detection: immediate feedback on broken changes. (5) Cost: high initial investment, but lower long-term cost vs manual retesting. Challenges: initial setup time, test maintenance as code evolves, flaky tests. What to automate: repetitive, stable, high-value test cases — regression tests, smoke tests, data-driven tests. What not to automate: exploratory testing (better done manually), usability testing, tests that change frequently, one-time tests. The ROI of test automation improves the more often a test is run — tests run on every commit are worth automating; tests run once a year are usually not.
25
What is continuous testing in CI/CD?
Continuous testing is the practice of running automated tests throughout the CI/CD pipeline — at every stage of development, from code commit to production deployment. Stages: (1) Pre-commit: local unit tests (developer's machine, fast). (2) Commit stage (CI): all unit tests + code quality checks on every commit/PR. (3) Integration stage: integration tests against real databases/services in a test environment. (4) Acceptance stage: E2E tests against a staging environment. (5) Performance stage: load tests (often on a schedule, not every commit). (6) Production: smoke tests after deployment, synthetic monitoring. Each stage gates the next — a failing stage blocks progression. Benefits: fast feedback (defects found close to when they were introduced — cheapest to fix), confidence in deployments, shorter release cycles. Tools: GitHub Actions, Jenkins, GitLab CI, CircleCI, Azure DevOps for orchestrating the pipeline.
26
What is manual testing vs automated testing?
Manual testing is performed by human testers who execute test cases without automation tools — exploring the application, simulating user behavior, and reporting defects. Advantages: (1) Human judgment — can detect UI/UX issues, inconsistencies that automated tests miss. (2) No setup investment. (3) Better for exploratory testing and usability. (4) Good for one-time tests. Disadvantages: slow, expensive at scale, error-prone (humans miss things), not repeatable exactly. Automated testing uses code/tools to execute tests. Advantages: speed (run 1000 tests in minutes), repeatability, 24/7 execution in CI, catches regressions instantly. Disadvantages: upfront investment in writing tests, maintenance burden, can't easily test subjective qualities. Best practice: automate what's valuable and stable; use manual testing for exploration, new features, accessibility, and human judgment scenarios. The goal is not to eliminate manual testing but to make automated testing handle the regression burden, freeing humans for high-value exploration.
27
What is a test plan?
A test plan is a document that outlines the testing strategy, objectives, scope, resources, timeline, and approach for a project. It defines what to test, how to test it, who tests it, when testing happens, and what the exit criteria are. Components of a test plan: (1) Scope: what features/functionality will be tested and what will not. (2) Test strategy: types of testing (unit, integration, E2E, performance, security). (3) Test environment: hardware, software, configurations needed. (4) Resources: team members, tools, and time. (5) Test schedule: timeline and milestones. (6) Entry/exit criteria: when testing starts and what constitutes "done." (7) Risk analysis: what risks could affect the test effort. (8) Defect management: how defects are reported, prioritized, and tracked. In agile, a heavy test plan document is replaced by sprint-level testing tasks and acceptance criteria in user stories.
28
What is a defect lifecycle?
The defect lifecycle (bug lifecycle) describes the states a defect goes through from discovery to closure. Typical states: New: defect reported for the first time. Assigned: assigned to a developer. Open: developer starts working on it. Fixed/Resolved: developer believes it's fixed. Ready for QA: ready for testing. Closed: QA verified the fix works. Reopened: fix didn't work, defect regressed, or invalid fix. Deferred: fix postponed to a later release. Rejected: not a real defect (works as designed, cannot reproduce). Additional states may include Duplicate (same issue reported before). Good defect reports include: title, environment, steps to reproduce, expected vs actual behavior, severity, priority, and screenshots/logs. Tools: Jira, GitHub Issues, Linear, Bugzilla. Priority (business impact) and severity (technical impact) are often different — a cosmetic bug on the homepage might be high priority but low severity.
29
What is the difference between severity and priority in bug reporting?
Severity describes the technical impact of the bug on the system — how badly it affects functionality. Levels: Critical/Blocker: system crashes, data loss, security breach, core functionality broken. High/Major: key feature doesn't work, significant workaround needed. Medium/Minor: feature partially works, workaround exists. Low/Trivial: cosmetic issue, typo, minor UI glitch. Priority describes the business urgency — how soon the bug should be fixed, regardless of technical severity. Priority is set by business stakeholders, not testers. Example of divergence: (1) High severity, low priority: a crash in a rarely used admin feature. (2) Low severity, high priority: a typo in the company name on the homepage (trivial technically, embarrassing to the business). Both dimensions are needed: a bug can be technically severe but unimportant to the business, or trivially small technically but critically visible to customers.
30
What is exploratory testing?
Exploratory testing is a simultaneous test design, execution, and learning activity where the tester explores the application without predefined test cases, using their intelligence, creativity, and domain knowledge to discover defects. Contrast with scripted testing (following predefined test scripts step-by-step). James Bach describes it as "simultaneous learning, test design, and test execution." Benefits: (1) Finds defects that scripted tests miss — a human tester notices unexpected behavior. (2) Adapts dynamically based on what's discovered. (3) Efficient — no script writing overhead. (4) Effective for newly developed features and complex interactions. Techniques: Session-based testing: time-boxed (60-90 min) focused exploration with a charter (mission). Pair testing: two testers exploring together. Personas: exploring as specific user types. Exploratory testing complements automated testing — automation handles known behaviors; exploration discovers the unknown.
31
What is parameterized testing?
Parameterized testing (data-driven testing) runs the same test logic with multiple different input/output combinations, reducing code duplication. Instead of writing 10 similar tests with different values, write one test method that accepts parameters and run it with 10 datasets. Examples: pytest: @pytest.mark.parametrize("input,expected", [(1, 2), (0, 1), (-1, 0)]) def test_increment(input, expected): assert increment(input) == expected. JUnit 5: @ParameterizedTest @ValueSource(ints = {1, 5, 10}). NUnit: [TestCase(1, 2)] [TestCase(0, 1)] public void TestIncrement(int input, int expected). xUnit: [Theory] [InlineData(1, 2)]. Use for: boundary value testing, equivalence classes, data-driven test tables. Keeps tests DRY while testing many scenarios. Combine with external data sources (CSV, JSON) for very large test datasets.
32
What is test-first vs test-last development?
Test-last (traditional): write production code first, then write tests to verify it. Problems: (1) Tests are shaped by the implementation rather than the requirements. (2) Tests verify what the code does, not what it should do. (3) Code is not written with testability in mind — harder to unit test. (4) Tests are often skipped when under time pressure. Test-first (TDD): write tests before code. Benefits: (1) Tests define behavior from requirements perspective. (2) Code is designed for testability from the start. (3) Tests are guaranteed to exist. (4) Red-green cycle gives immediate feedback. Which is better: for complex business logic with clear requirements — test-first (TDD) produces better-designed, better-tested code. For exploratory or UI prototyping work — test-last is often more pragmatic. Both are vastly better than no tests. Even in test-last workflows, writing tests immediately after (not months later) captures the context and produces better tests.
33
What is white-box vs black-box testing?
Black-box testing: testing without knowledge of the internal implementation. The tester only knows inputs and expected outputs — treating the system as a "black box." Tests are derived from requirements/specifications. Techniques: equivalence partitioning, boundary value analysis, decision table testing. Types: functional testing, acceptance testing, E2E testing. White-box testing (clear-box, glass-box): testing with full knowledge of the internal implementation. Tests are designed to exercise specific code paths, branches, loops, and internal logic. Techniques: statement coverage, branch coverage, path coverage. Types: unit testing (developer writing tests for their own code). Grey-box testing: partial knowledge — the tester knows some of the internal structure (database schema, API contracts) but not necessarily the full implementation. Integration testing is often grey-box. Best practice: use both — black-box tests verify requirements from the user's perspective; white-box tests ensure internal correctness and coverage.
34
What is a test case?
A test case is a set of conditions under which a tester determines whether a system or feature works correctly. A well-written test case contains: (1) Test case ID: unique identifier. (2) Title: what is being tested. (3) Preconditions: state the system must be in before the test. (4) Test data: specific inputs to use. (5) Test steps: step-by-step actions to perform. (6) Expected result: what should happen if the system works correctly. (7) Actual result: what actually happened (filled in during execution). (8) Pass/Fail status. (9) Postconditions: state after the test. Good test cases are: clear, concise, repeatable, objective, and traceable to requirements. In automated testing, the test case is encoded in the test function/method itself — the docstring or name serves as the title, assertions as expected results. Tools for managing test cases: Zephyr, TestRail, qTest, Xray.
Practical knowledge for developers with hands-on experience.
01
What is the Red-Green-Refactor cycle in TDD?
The Red-Green-Refactor cycle is the core TDD workflow: Red: write a test for a small, specific behavior that you're about to implement. Run it — it must fail (confirm it's actually testing something). A test that passes before code is written is meaningless. Green: write the minimum amount of production code to make the failing test pass. Don't write more than needed — resist the urge to over-engineer. The goal is just to make it green. Refactor: clean up both the test code and production code while keeping all tests green. Remove duplication, improve naming, extract methods, apply patterns. The test suite ensures refactoring doesn't break behavior. Repeat for the next behavior. The cycle should be short — minutes, not hours. This discipline produces: design that emerges from requirements (not speculation), complete test coverage for implemented features, and clean, refactorable code. Kent Beck: "Write tests until fear is transformed into boredom."
02
What is the FIRST principle in unit testing?
The FIRST acronym describes properties of good unit tests: F — Fast: unit tests should run in milliseconds. A slow test suite discourages running tests frequently. If tests are slow, they're not unit tests (they might be integration tests with real I/O). I — Isolated/Independent: tests should not depend on each other or share state. Any test should be runnable alone in any order. R — Repeatable: same result every time, regardless of environment, time, or external state. No randomness without seed, no real time dependencies, no network calls. S — Self-validating: test produces a clear pass/fail result — no manual inspection of log output needed. The assertion determines pass/fail automatically. T — Timely (or Thorough): written at the right time — before or alongside the production code (TDD), not months later. Also interpreted as thorough — test the full range of behaviors including edge cases. These principles guide design decisions when writing tests.
03
What is test coverage and what are the risks of over-relying on it?
Test coverage measures what percentage of code is executed by tests — a useful indicator for finding untested code. Types: statement, branch, function, path coverage. Risks of over-relying on coverage metrics: (1) Coverage ≠ quality: assertTrue(true) increases coverage but tests nothing meaningful. 100% coverage with meaningless assertions is worthless. (2) Goodhart's Law: "When a measure becomes a target, it ceases to be a good measure." Teams game the metric by writing tests that cover code without asserting correct behavior. (3) Missing edge cases: 100% line coverage doesn't mean all branches, values, or edge cases are tested. (4) False confidence: high coverage can make teams complacent. (5) Wrong incentives: chasing coverage leads to testing implementation details instead of behavior, making tests brittle to refactoring. Healthy use: track coverage to identify gaps (areas with zero coverage are risky); don't use arbitrary thresholds as goals. Combine with mutation testing for quality assessment.
04
What is mutation testing?
Mutation testing evaluates the quality of a test suite by intentionally introducing small bugs (mutations) into the production code and checking whether the tests catch them. A mutant is a modified version of the code. If tests fail when a mutant is introduced, the mutant is "killed" (good). If tests still pass, the mutant "survives" (bad — your tests aren't catching this type of defect). Common mutations: changing + to -, > to >=, true to false, removing method calls, changing return values. Mutation score = (killed mutants / total mutants) × 100%. A high mutation score indicates tests are actually catching bugs. Tools: Stryker (JavaScript/.NET), PIT/Pitest (Java), mutmut (Python), Infection (PHP). Mutation testing is the gold standard for test suite quality — far more meaningful than code coverage. Downside: computationally expensive (generates and tests many mutants).
05
What is property-based testing?
Property-based testing (PBT) generates random input data and checks that certain properties/invariants always hold, rather than testing specific example inputs. Instead of writing: "given input 5, expect output 25" (example-based), write: "for any non-negative integer n, square(n) should be >= n." The PBT framework generates hundreds of random inputs and runs each through the property. When a failure is found, it shrinks the input to the minimal failing case. Tools: QuickCheck (Haskell — the original), fast-check (JavaScript), Hypothesis (Python — excellent), FsCheck (F#/.NET), jqwik (Java). Properties to test: commutativity (a+b = b+a), associativity, idempotence (applying twice = applying once), inverse (encode then decode = identity), round-trips (serialize then deserialize = original). PBT finds edge cases humans never think to test — it discovered the infamous "equal adjacent elements" bug in sorting algorithms that passed all hand-written tests.
06
What is contract testing?
Contract testing verifies that a service (provider) and its consumers agree on the interface — without requiring both to be running simultaneously. Most relevant in microservices. The consumer defines what it expects from the provider in a contract; the provider verifies it satisfies that contract. Two types: Consumer-driven contract testing: the consumer writes the contract (expectations); the provider runs tests against it. Most common and recommended — Pact framework implements this. Provider-driven: the provider defines the contract; consumers verify they conform. Pact: the leading consumer-driven contract testing tool. Consumer generates a pact file (interactions: request → response); provider verifies each interaction against the real code. Pact Broker: central storage for pact files with compatibility matrix. Benefits: (1) Test integrations without running all services. (2) Catch breaking API changes early (CI fails before deployment). (3) Teams work independently. (4) Faster than full integration tests. Alternative: schema-based contracts (OpenAPI, GraphQL schema checks).
07
What is test-driven development in the context of legacy code?
Applying TDD to legacy code (code without tests) requires different techniques than greenfield TDD. The challenge: you can't refactor (safely) without tests, but you can't write tests easily for untested code (tight coupling, global state, hardcoded dependencies). Michael Feathers' book "Working Effectively with Legacy Code" addresses this. Techniques: (1) Characterization tests: write tests that document current behavior (even if wrong) to prevent regressions while refactoring. (2) Seam identification: find points in the code where you can substitute behavior for testing — object seams (dependency injection), preprocessor seams, link seams. (3) Extract and override: extract hard-coded dependencies into methods you can override in test subclasses. (4) Sprout method/class: add new functionality in a new, testable method/class called from the legacy code. (5) Wrap method: wrap the existing method with testable code. Apply the "Boy Scout Rule" — always leave the code cleaner than you found it, adding tests as you touch it.
08
What is the test pyramid vs the testing trophy?
The Testing Pyramid (Martin Fowler, Mike Cohn): many unit tests at the base, fewer integration tests in the middle, very few E2E tests at the top. Emphasizes unit tests as the foundation. The Testing Trophy (Kent C. Dodds, popularized in the JavaScript/React community): static analysis at the base, fewer unit tests, a wide middle layer of integration tests, and a small top layer of E2E tests. Key insight: "Write tests. Not too many. Mostly integration." The trophy argues that integration tests (which test components working together) provide more confidence per test than pure unit tests (which may over-mock and test implementation details). Pure unit tests with extensive mocking can give false confidence and break on refactoring that doesn't change behavior. The right balance depends on the project: backend business logic → emphasize unit tests (clear units of work); frontend/API → integration tests give better confidence (test the whole request lifecycle). Both agree: too many E2E tests = slow, brittle pipeline.
09
What is snapshot testing?
Snapshot testing captures the output of a component/function at a point in time and saves it as a reference snapshot file. On subsequent test runs, the output is compared to the saved snapshot — if it differs, the test fails. Popularized by Jest for React component testing: expect(component.render()).toMatchSnapshot();. The first run creates a .snap file. On changes: if intentional, update snapshots (jest --updateSnapshot); if unintentional, fix the bug. Benefits: easy to write, catches unintended UI changes, good for large complex outputs. Pitfalls: (1) Brittle: any UI change (even intentional) requires snapshot updates. (2) Meaningless diffs: large snapshot diffs are hard to review. (3) False positives: devs blindly update snapshots without verifying the change is correct. (4) Implementation coupling: snapshots capture implementation details. Best used for: stable, deterministic outputs (serializers, formatters, API responses). Avoid for frequently changing UI components — use explicit behavioral assertions instead.
10
What is the difference between unit tests and integration tests for databases?
For code that interacts with databases, there are two approaches. Unit testing with mocks: replace the database with a mock/stub. The repository or query is tested in isolation. Pros: fast, no setup. Cons: doesn't test the actual SQL query, misses database-specific behavior (type coercions, constraint violations, query performance). Mocked tests can pass while the real database query is broken. Integration testing with a real database: use a real database (same type as production). Options: (1) In-memory databases (SQLite for EF Core, H2 for Java) — fast but may behave differently from production PostgreSQL/MySQL. (2) Testcontainers: spin up a real Docker container of PostgreSQL/MySQL per test run — identical to production, slower to start. (3) Shared test database: use a dedicated test schema/database — fast start, but requires careful cleanup. Best practice: use Testcontainers for integration tests. For unit tests of business logic, mock the repository interface. Test actual repository/query implementations in integration tests against a real database.
11
How do you test asynchronous code?
Testing async code requires test frameworks that support async operations. JavaScript/Jest: return a Promise or use async/await: test("fetches user", async () => { const user = await fetchUser(1); expect(user.name).toBe("Alice"); });. Use done callback for callback-style async. Mock async functions: jest.fn().mockResolvedValue(data). C# (xUnit/NUnit): async Task test methods are natively supported: [Fact] public async Task TestAsync() { var result = await service.GetAsync(); Assert.Equal("expected", result); }. Python (pytest-asyncio): @pytest.mark.asyncio async def test_fetch(): result = await fetch_data(); assert result == expected. Common pitfalls: forgetting to await (test passes even if async code throws); not handling rejection (unhandled promise rejections may not fail tests in some frameworks); real timers causing timeouts. Use fake timers (jest.useFakeTimers()) for time-dependent async code. waitFor utility (React Testing Library) waits for assertions to pass in async UI tests.
12
What is test containerization with Testcontainers?
Testcontainers is an open-source library that provides lightweight, throwaway Docker container instances for integration tests. It starts real infrastructure (PostgreSQL, Redis, Kafka, Elasticsearch, etc.) as Docker containers for test duration, then stops and removes them. Example in Java: @Container PostgreSQLContainer postgres = new PostgreSQLContainer("postgres:15"); @Test void testSave() { var ds = DataSourceBuilder.create().url(postgres.getJdbcUrl()).build(); ... }. Benefits: (1) Real dependencies: same database type as production — no SQLite vs PostgreSQL behavior differences. (2) Isolation: each test run gets a fresh container. (3) No shared state: no leftover data between test runs. (4) CI/CD compatible: works in any environment with Docker. (5) No manual setup: no need to maintain a shared test database server. Available for: Java, .NET, Python, Go, JavaScript, Rust. Slower than in-memory databases — offset by running in parallel. The gold standard for database integration tests.
13
What is test double vs mock framework?
A test double is any replacement for a real dependency in a test (the concept). A mock framework is a library that helps create test doubles (the tool). Without a framework, creating mocks means manually implementing interfaces: tedious and verbose. Mock frameworks generate these implementations dynamically at runtime. Examples: Moq (C#): var mock = new Mock<IUserService>(); mock.Setup(s => s.GetUser(1)).Returns(new User()); var service = mock.Object;. Mockito (Java): UserService mockService = mock(UserService.class); when(mockService.getUser(1)).thenReturn(new User());. Jest (JavaScript): const mockFn = jest.fn().mockReturnValue(42);. unittest.mock (Python): with patch('module.ClassName') as MockClass: MockClass.return_value.method.return_value = 'value'. Features of mock frameworks: setup return values, verify calls, argument matchers, capturing arguments, partial mocks (spies). Choose a framework with good IDE integration and readable error messages (Moq's error messages are excellent).
14
What is stubbing HTTP calls in tests?
When your code makes HTTP calls to external APIs, you need to stub those calls in tests to avoid: network dependency, rate limits, costs, unreliable external services, and test data pollution. Approaches: WireMock: runs a real HTTP server locally that you configure to return specific responses: stubFor(get(urlEqualTo("/api/users/1")).willReturn(aResponse().withBody(json).withStatus(200)));. Verify calls were made. MSW (Mock Service Worker — JavaScript): intercepts fetch/XHR at the network level using Service Workers: rest.get('/api/users', (req, res, ctx) => res(ctx.json({ name: 'Alice' }))). Works in both browser and Node.js tests. nock (Node.js): intercepts HTTP requests in Node: nock('https://api.example.com').get('/users/1').reply(200, { name: 'Alice' });. pytest-responses / responses (Python): similar for Python requests library. Test both success and error responses (404, 500, timeouts) to ensure resilience code is exercised.
15
What is the testing scope for microservices?
Testing microservices requires a strategy adapted to their distributed nature. The testing honeycomb (Spotify model): Unit tests: test domain logic in isolation — fast, plentiful. Integration tests: test service internals (service → database, service → cache) — use Testcontainers. Contract tests: verify API contracts between services — use Pact (consumer-driven). These replace the need for full end-to-end tests across many services. Component tests: test a service in isolation with all its real internal dependencies but with external services stubbed — use WireMock/stubby. System tests (E2E): test critical end-to-end journeys across multiple services — kept minimal. Key principles: (1) Don't test other teams' services — trust their tests + contract tests. (2) Don't integrate in CI unless necessary — use contracts. (3) Each service owns its own tests. (4) Test environment parity: staging environment for E2E. (5) Consumer-driven contracts: consumers define what they need from providers — prevents provider breaking changes. Most testing value comes from unit + integration + contract tests.
16
What is chaos engineering?
Chaos engineering is the practice of intentionally introducing failures into a system in production (or production-like environments) to discover weaknesses before they cause unplanned outages. Originated at Netflix (Chaos Monkey, the Simian Army). The discipline: formulate a hypothesis about steady-state behavior, introduce controlled failures, observe deviations, learn and improve. Common experiments: terminate random instances, inject network latency/packet loss, simulate datacenter failure (Chaos Kong), exhaust CPU/memory, introduce database slowdowns. Tools: Chaos Monkey (Netflix), Gremlin, LitmusChaos (Kubernetes), AWS Fault Injection Service, Istio (service mesh fault injection). Principles: (1) Start with a well-defined hypothesis. (2) Run experiments in production (or a faithful replica). (3) Have automated rollback. (4) Limit blast radius initially. (5) Automate experiments to run continuously. Chaos engineering reveals: unhandled failure modes, missing circuit breakers, cascading failures, single points of failure that testing environments miss.
17
What is code review in the context of quality assurance?
Code review is the practice of having peers examine source code changes before they are merged — a human quality gate that complements automated testing. What code reviews catch that automated tests miss: (1) Logic errors: "this algorithm is wrong for edge case X." (2) Design issues: poor abstraction, wrong responsibility. (3) Security vulnerabilities: SQL injection risk, exposed secrets. (4) Readability: naming, comments, clarity. (5) Missing tests: "where are the tests for this behavior?" (6) Non-obvious bugs: off-by-one errors, race conditions. (7) Domain knowledge: "this violates our business rule about X." Best practices: (1) Review the PR diff against requirements, not just the code in isolation. (2) Prefer small PRs (easier to review). (3) Author should explain non-obvious decisions. (4) Use a checklist for consistent reviews. (5) Keep reviews constructive, not personal. (6) Automated linting/SAST reduces review burden on style/syntax. (7) Require at least one approval before merge. Tools: GitHub Pull Requests, GitLab MR, Crucible, Gerrit.
18
What is static analysis and linting in the context of testing?
Static analysis analyzes code without executing it, using automated tools to find potential bugs, security vulnerabilities, code smells, and style violations. It complements dynamic testing by catching issues that tests miss. Types: Linters: enforce code style and catch common mistakes (ESLint for JS, Pylint/Flake8 for Python, RuboCop for Ruby, PHPStan/Psalm for PHP, StyleCop for C#). SAST (Static Application Security Testing): find security vulnerabilities (Semgrep, CodeQL, Checkmarx, SonarQube). Type checkers: TypeScript, mypy (Python), Flow — catch type errors at compile time. Complexity analyzers: identify overly complex methods (cyclomatic complexity > 10 = hard to test). Dependency analysis: SCA tools find vulnerable dependencies (Snyk, OWASP Dependency-Check, npm audit). Integrate in CI pipeline as a pre-test gate — fail the build on linting errors and high-severity security findings. Running static analysis gives instant feedback without slow test execution. "Static analysis is a test that never needs to be written."
19
What is the role of a QA engineer?
A QA (Quality Assurance) engineer is responsible for ensuring software quality throughout the development lifecycle — not just finding bugs, but preventing them. Responsibilities: (1) Requirements review: identify ambiguities and testability issues before development. (2) Test planning and strategy: define what, how, and when to test. (3) Test case design: create comprehensive test cases covering happy paths, edge cases, and error scenarios. (4) Test automation: write and maintain automated test suites (E2E, API, performance). (5) Defect reporting: clear, reproducible bug reports. (6) Regression testing: ensure new changes don't break existing functionality. (7) Performance and security testing coordination. (8) CI/CD pipeline: ensure tests run automatically. Modern QA evolution: the "shift-left testing" movement integrates QA earlier — QA engineers work alongside developers, review code, and help developers write better unit tests. Some organizations move to "quality engineering" where all engineers are responsible for quality, with QA providing expertise and tooling. The role is evolving from "manual tester" to "quality enabler."
Deep expertise questions for senior and lead roles.
01
What is the Outside-In TDD approach?
Outside-In TDD (also known as London School TDD or "Mockist TDD") starts from the outside (acceptance/integration level) and works inward, using mocks to define collaborator interfaces. Contrast with Inside-Out TDD (Chicago/Detroit School) which starts with domain objects and builds up. Outside-In workflow: (1) Write a failing high-level acceptance test (E2E or integration). (2) Write a failing unit test for the outermost class (controller/handler). (3) Mock collaborators (services, repositories) — this defines their interface. (4) Make the unit test pass. (5) Write unit tests for mocked collaborators, repeat down the stack. (6) Eventually, the acceptance test passes. Benefits: design emerges from requirements outward; interfaces are designed from the consumer's perspective. Downsides: heavy use of mocks can create brittle tests tied to implementation; requires discipline to avoid over-mocking. Inside-Out TDD is better for domain logic; Outside-In for systems with clear interface contracts. Many practitioners combine both — start outside for overall direction, go inside-out for domain core.
02
What is the difference between sociable and solitary unit tests?
Terms coined by Jay Fields (and discussed by Martin Fowler): Solitary unit tests (mockist/London School): test a single class/function in complete isolation — all dependencies are replaced with test doubles. Strict isolation ensures you know exactly what's being tested. Risk: tests may pass while the real integration is broken (heavy mocking can mask real bugs). Sociable unit tests (classicist/Detroit School): test a unit with its real dependencies (other units) where those dependencies are stable and fast. Only external I/O (databases, APIs, filesystem) is replaced. Tests reflect more realistic behavior. Risk: when a test fails, it's harder to identify which unit caused the failure. Which to choose: use solitary tests for units with complex logic where isolation is valuable (complex business rules, algorithms). Use sociable tests for coordinating units where the interaction matters (domain aggregates collaborating). Integration tests are always sociable. The debate is less about which is right and more about where to draw the isolation boundary.
03
What is trunk-based development and how does it relate to testing?
Trunk-based development (TBD) is a source control branching strategy where developers integrate their work into the main branch (trunk) frequently — at least once per day, often multiple times. All developers commit to a single shared branch, with no long-lived feature branches. TBD's impact on testing: (1) Comprehensive test suite is mandatory: with everyone committing to main continuously, a fast, reliable test suite is the only safety net against breaking changes. (2) Feature flags: incomplete features are committed but hidden behind feature flags — tests can verify both enabled and disabled states. (3) Strict CI: every commit must pass all tests before merging — test failure blocks the pipeline. (4) Speed matters: a slow test suite (> 10 min) discourages frequent commits — optimize relentlessly. (5) Test quality: flaky tests are particularly harmful in TBD — any flakiness blocks everyone. TBD is the enabling practice for continuous deployment. It requires maturity in testing, feature flags, and CI/CD tooling.
04
What is approval testing?
Approval testing (golden master testing) is a testing technique that captures the complete output of a system (a "golden master") and compares subsequent runs against it, failing if anything changes. Similar to snapshot testing but for larger, more complex outputs (entire documents, API responses, rendered HTML, serialized objects). Process: (1) Run the system once, capture output. (2) Review and approve it as correct. (3) Subsequent runs compare against the approved output — any diff fails the test. (4) When an intentional change is made, re-approve the new output. Tools: ApprovalTests (multi-language library), Verify (C# — excellent), jest-snapshot. Best for: testing legacy code without tests (characterization testing), testing complex output structures, serialization, generated code, HTML templates. Advantages: fast to write for complex outputs, catches any change. Pitfalls: same as snapshot tests — diffs must be reviewed carefully, not blindly approved. Combine with Diff tools for readable approval reviews.
05
What is the testing diamond vs testing pyramid?
Beyond the classic pyramid, teams have proposed variations: Testing Diamond (or Honeycomb): wide in the middle (service/integration tests), narrow at top (E2E) and bottom (unit). Popularized by Spotify. The philosophy: integration tests (testing a service with all its real internal components) provide the best ROI — they test realistic scenarios without the brittleness of full E2E tests, and more confidence than unit tests with mocks. Unit tests are used only for complex algorithmic logic. Testing Honeycomb (Spotify): same as diamond but explicitly names the middle layer "service integration tests." Testing Trophy (Kent C. Dodds): similar — wide integration middle, with static analysis at the very base. When diamond/honeycomb makes sense: API-centric services (backends, microservices) where integration tests with real databases (Testcontainers) are fast, reliable, and test the full stack; frontend code where integration tests (React Testing Library) test component + hooks + rendering together. Pyramid still makes sense: complex domain logic with many rule combinations — unit tests scale better.
06
What is load testing and how do you design effective load tests?
Load testing verifies system behavior under expected and peak production load. Designing effective load tests: (1) Define SLAs: what response time and error rate are acceptable under load? (p95 < 200ms, error rate < 0.1%). (2) Realistic traffic patterns: use production access logs to model real user behavior (which endpoints, which request sequences, realistic data). (3) Ramp-up/ramp-down: gradually increase load — sudden spikes reveal different issues than gradual build-up. (4) Think time: add realistic pauses between user actions. (5) Data variety: use realistic, varied data — avoid testing only one user ID (may hit cache for all requests). (6) Test environment: production-like hardware/configuration — results from a dev laptop mean nothing. (7) Baseline first: establish baseline performance, then test changes against it. (8) Monitor everything: CPU, memory, DB connections, GC, application metrics — not just response time. (9) Isolate bottlenecks: use flame graphs and profilers when load tests reveal slowdowns. Tools: k6 (JavaScript scripting, great DX), Gatling (Scala DSL, excellent HTML reports), JMeter (GUI, widely used).
07
What is the role of testing in Domain-Driven Design?
In Domain-Driven Design (DDD), testing strategy aligns with the architecture. (1) Domain model unit tests: the richest part to test — pure domain logic, aggregates, value objects, domain events, domain services. These are completely isolated (no mocks needed — domain objects have no infrastructure dependencies). Use TDD here — tests drive the domain model design. Example: var order = new Order(); order.AddItem(product, 2); Assert.Equal(2, order.ItemCount);. (2) Application service integration tests: test use cases/application services with real repositories but mocked external services. (3) Repository integration tests: test the repository implementations against a real database (Testcontainers). (4) Anti-corruption layer tests: verify translation between bounded contexts. (5) Domain event tests: verify events are raised on state changes. Bounded contexts define natural test boundaries — each context tests independently. The hexagonal architecture (ports and adapters) makes domain testing natural: the domain has no infrastructure dependencies, so pure unit tests are easy to write.
08
What are test anti-patterns and how do you avoid them?
Common test anti-patterns that reduce test value: (1) Testing implementation details: tests break when you refactor without changing behavior. Fix: test outcomes/behavior, not internal calls. (2) Over-mocking: mock everything including internal collaborators — tests pass but real integration is broken. Fix: mock only external dependencies (DBs, APIs, filesystems). (3) The "Liar": test passes but doesn't actually test anything (assertion is always true, or testing the mock itself). Fix: run code coverage + mutation testing. (4) Tests too large: each test asserts too many things — hard to identify failures. Fix: one concept per test. (5) Shared mutable state: static fields, global variables shared between tests. Fix: initialize in SetUp, use DI. (6) Slow tests: tests that call real services, sleep, or do heavy I/O. Fix: mock I/O, use in-memory alternatives. (7) Non-deterministic tests: use real time, random data, external services. Fix: inject clock, seed random, mock external. (8) Excessive test setup: 50 lines of Arrange before Act. Fix: test data builders (builder pattern), object mother pattern. (9) Commented-out tests: silently skipping failing tests. Fix: fix or delete them.
09
What is test data management?
Test data management (TDM) involves creating, maintaining, and managing the data required for testing — ensuring tests have appropriate, isolated, realistic data without causing issues. Challenges: sensitive production data (PII — GDPR), maintaining data across test runs, performance with large datasets, data freshness, and environment consistency. Strategies: (1) Data factories/builders: programmatically create test data on demand using builder patterns — no hardcoded IDs, fresh data per test. Libraries: FactoryBot (Ruby), Faker (multi-language for fake data), Bogus/.NET. (2) Database seeding: pre-populate with a known starting state per test run. (3) Transaction rollback: wrap each test in a transaction, roll back after — no cleanup needed. (4) Anonymization/synthetic data: generate fake but realistic data instead of using production PII. (5) Test data API: expose endpoints in test environments to create/reset state. (6) Data virtualization: mask and serve subsets of production data. (7) Contract-based data: use Pact contracts to define the minimum data structure, test with that. Good TDM is a critical but often neglected part of test infrastructure.
10
What is synthetic monitoring vs passive monitoring?
Synthetic monitoring (active monitoring): regularly executes pre-scripted transactions against your application to verify it's working — simulating user behavior proactively. Examples: ping a login endpoint every minute, run a checkout flow every 5 minutes. Detects issues before real users do. Tools: Datadog Synthetics, New Relic Synthetics, Pingdom, Checkly. Use for: SLA monitoring, uptime checks, critical user journey verification post-deployment. Passive monitoring (Real User Monitoring — RUM): observes actual user traffic and measures performance from real interactions — no synthetic traffic. Tools: Datadog RUM, Sentry Performance, Dynatrace, Google Analytics. Use for: understanding real user experience, identifying performance issues by geography or device. Combination: synthetic for proactive alerting (before users notice), passive for understanding real impact. In the testing context, synthetic monitoring is often called "production testing" or "testing in production" — running a subset of E2E tests against real production as smoke tests after deployment. Canary deployments combine monitoring with gradual rollout.
11
What are the principles of good test design?
Principles for creating maintainable, valuable tests: (1) Test behavior, not implementation: tests should verify what the system does (from the caller's perspective), not how it does it internally. Refactoring should not break tests. (2) One assertion per test (ideally): each test verifies one concept — when it fails, you immediately know what's wrong. (3) DRY is secondary to clarity: test code prioritizes readability over DRY. Slight duplication is acceptable if it makes tests clearer. Use helper methods for complex setup, not at the cost of test readability. (4) Arrange-Act-Assert: consistent structure. (5) Descriptive naming: Given_ValidUser_When_Login_Then_ReturnsJwtToken() or login with valid credentials returns JWT token. Names document behavior. (6) Tests as documentation: a new developer should understand the system's behavior by reading the tests. (7) No logic in tests: avoid if/else, loops in tests — they can contain bugs themselves. (8) Independent and idempotent: run in any order, any number of times. (9) Test at the right level: don't use E2E tests for something unit tests can cover faster and more precisely. (10) Tests are production code: maintain them with the same care.