SDET Interview Questions: Complete Guide With Answers
SDET Interview Questions: Comprehensive Preparation Guide
An SDET (Software Development Engineer in Test) is not a QA tester who learned some coding. SDETs write code to enable testing at scale. They design test architecture, build automation frameworks, and solve problems that would make traditional QA automation specialists say “that’s too complex.” Interviewers assess whether you can architect testing solutions, think like a developer, and understand the business impact of quality.
The technical bar for SDET interviews is high. You’ll be asked to write code, reason about algorithm trade-offs, and handle ambiguous problems. This guide covers the questions you’ll see, organized by topic and difficulty, with answers that show what interviewers are actually looking for.
Testing Fundamentals
Explain the test pyramid and why it matters.
The test pyramid has three layers. At the bottom, unit tests are the foundation: fast, isolated, many of them. A unit test exercises a single function or class without external dependencies. Unit tests run in milliseconds and catch bugs at the component level.
The middle layer is integration tests. These verify that components work together. A service test might hit a database or call another service. Integration tests are slower than unit tests but catch wiring bugs and data flow issues that unit tests miss.
The top is end-to-end tests. These test the entire system from user input to database. An E2E test logs in, navigates the app, and verifies the UI shows the expected result. E2E tests are slow, brittle, and expensive to maintain. You have fewer of them.
The pyramid shape is critical. Many teams invert the pyramid: few unit tests, many E2E tests. This is expensive and slow. The inverse pyramid (many E2E tests) is a sign of problems. A proper pyramid has a solid foundation of fast unit tests, reducing the need for slower E2E tests.
The pyramid isn’t rigid. Some teams use a diamond (more integration tests relative to unit tests) or a hourglass (unit and E2E tests, fewer integration tests). The principle remains: optimize for fast feedback by writing tests at the right level of isolation.
What are the differences between unit, integration, and end-to-end testing?
Unit tests are isolated. A unit test for a function that calculates tax mocks the database and clock. It exercises only the function. The test controls all inputs and verifies the output. Unit tests typically run in microseconds to milliseconds.
Integration tests involve real collaborators. A test for the order service calls the real payment service (or a test double that behaves like the real service). Integration tests verify that components communicate correctly. They’re slower but catch real-world issues that unit tests miss, like “the function works but sends malformed data to the collaborator.”
End-to-end tests exercise the entire system. They’re external tests: the test doesn’t know about the internal structure. It uses the system like a user does. For a web app, an E2E test uses a browser and navigates the UI. For an API, an E2E test sends HTTP requests. E2E tests are slow and brittle but catch user-facing issues.
The choice depends on what you’re testing. Core business logic should have excellent unit test coverage. Data flows and service boundaries need integration tests. Critical user journeys need E2E tests. Avoid the trap of testing everything at the E2E level; that’s slow and fragile.
How should test coverage be measured and what are its limitations?
Code coverage measures the percentage of code executed by tests. Line coverage counts lines hit; branch coverage counts conditional branches taken. 80% line coverage might look good, but if the uncovered 20% is error handling, you’ve missed critical testing.
Coverage is a useful metric, but it’s not a goal. 100% coverage is meaningless if tests don’t assert anything. A test that runs code but doesn’t check the result gives false confidence. Coverage should inform where to add tests, not be the target itself.
Good practices: measure coverage for core business logic (aim for 80%+). Error handling and edge cases deserve coverage. Infrastructure and boilerplate code can have lower coverage. Use coverage reports to find gaps, not as a scorecard.
Limitations: coverage doesn’t measure test quality. High coverage with weak assertions is worse than lower coverage with rigorous tests. Integration coverage is hard to measure but important. Code coverage tools can’t measure user workflow coverage (does the test cover a realistic user scenario?). Use coverage as one signal among many.
What are test doubles and how do they differ?
A test double is a substitute for a real dependency. The differences matter.
A stub returns canned responses. You might stub a database to return a specific user record. Stubs don’t care about how they’re called; they return the same response regardless. Stubs are simple and appropriate when you only care about the response, not the interaction.
A mock records interactions and allows assertions about them. A test might assert that a logging function was called exactly once with specific arguments. Mocks are useful when the interaction itself matters, not just the result. Mocks are also more fragile; they require exact matching of calls.
A spy is a real object with recorded interactions. Unlike a mock, a spy wraps a real implementation. It records calls but executes the real logic. Spies are useful when you want to verify that an existing function was called while still executing its real behavior.
A fake is a simplified real implementation. A fake in-memory database stores data without persistent storage. Fakes are more complex than mocks but closer to reality. Fakes catch bugs that mocks miss because they actually do work, just simplified.
A dummy is an object passed to satisfy a function signature but never used. If a function requires five parameters but a test only cares about one, the others are dummies. Dummies are placeholders; don’t overthink them.
Explain BDD and TDD and their relationship to test automation.
Test-Driven Development (TDD) means writing tests first, then code. Write a test that fails, write minimal code to pass it, refactor. TDD makes you think about the interface and edge cases before implementing. It creates a tight feedback loop. The downside is that TDD is slower initially; you’re writing more code.
Behavior-Driven Development (BDD) extends TDD with a focus on business behavior. BDD tests are written in a human-readable language (Gherkin, for example: “Given a user is logged in, when they click submit, then the form is saved”). This makes tests easier for non-developers to understand and maintains a connection between requirements and tests.
For automation, BDD tools like Cucumber let you define scenarios in plain English, then map them to code (step definitions). A developer writes the step definitions; non-technical people can write scenarios. This bridges the gap between QA and developers.
TDD is valuable for ensuring code is testable and driving good design. BDD is valuable for ensuring tests reflect business requirements. They’re complementary; use both when it makes sense. For quick feature development, strict TDD might slow you down. For complex business logic, BDD ensures understanding.
What is shift-left testing and why is it important?
Shift-left means moving testing earlier in the development process. Traditionally, testing happened at the end: code was written, then QA tested it, and bugs were found late. Shift-left means developers test their code as they write it, testers get involved during requirements, and automated tests catch bugs in CI before they reach QA.
Benefits: bugs are caught earlier and cost less to fix. Developers understand requirements better when QA is involved upfront. Automated tests in CI provide fast feedback. The entire team feels ownership of quality, not just QA.
Implementation: unit tests during development, integration tests in PR reviews, contract tests for service boundaries, E2E tests in a staging environment, and production monitoring to catch issues that tests missed. Testing is continuous, not a phase at the end.
How do you identify and eliminate flaky tests?
Flaky tests pass sometimes and fail sometimes, even when the code doesn’t change. They’re worse than failing tests because they erode trust in the test suite. If tests fail randomly, developers stop trusting them and stop investigating failures.
Common causes: timing issues (tests assume a certain execution speed; if the system is slow, assertions fail), external dependencies (database goes down, network is slow, API returns different data), test isolation (one test leaves state that affects another), and randomness (tests depend on the current time or random data).
To eliminate flaky tests, identify them first. Rerun failures multiple times. If a test fails intermittently, it’s flaky. Run tests in different orders and environments; flakiness often depends on timing or environment.
Then fix them. Remove dependencies on timing (use test doubles instead of waiting). Ensure test isolation (each test starts clean). Avoid randomness where possible; if you need it, seed the random generator so failures are reproducible. Mock external dependencies.
For E2E tests, flakiness is nearly inevitable. Mitigate with explicit waits (wait for an element to appear, not a fixed sleep), retry mechanisms (retry a step if it fails), and logging (capture what happened when it fails, so you can diagnose).
Test Automation Frameworks
Compare Selenium, Playwright, and Cypress for browser automation.
Selenium is the oldest and most widely supported. It works with every browser and language. Many companies have years of Selenium code. The downside is it’s slower than newer tools and has frustrating synchronization issues; you often need explicit waits for elements to appear.
Playwright is newer and faster. It communicates directly with the browser rather than through WebDriver, reducing overhead. Playwright has excellent support for modern browsers (Chromium, Firefox, WebKit) and handles synchronization better than Selenium. Playwright is easier to debug; you can pause tests and inspect state.
Cypress is browser-only (JavaScript) but deeply integrated with the browser. It has excellent documentation and is considered the easiest to learn. Cypress runs inside the browser, allowing it to synchronize perfectly; no explicit waits needed. The downsides are that Cypress doesn’t support multiple windows or tabs easily, and you’re locked into JavaScript.
For new projects, Playwright or Cypress are better choices than Selenium. For existing Selenium codebases, the investment is high to migrate; Selenium works fine. For specific needs (IE support, multiple languages), Selenium might be your only choice.
In an interview, discuss your hands-on experience. If you’ve used Selenium, talk about synchronization strategies. If you’ve used Playwright or Cypress, discuss debugging and test structure. Avoid picking a framework you’ve never used just because it’s trendy.
Explain the Page Object Model and why it improves test maintenance.
The Page Object Model (POM) treats each page (or component) as an object. Instead of tests writing CSS selectors directly, tests call methods on the page object. The page object encapsulates selectors and interactions.
// Without POM
test('user can login', async () => {
await page.fill('input[name="email"]', 'user@example.com');
await page.fill('input[name="password"]', 'password');
await page.click('button[type="submit"]');
expect(await page.textContent('h1')).toBe('Dashboard');
});
// With POM
const loginPage = new LoginPage(page);
await loginPage.login('user@example.com', 'password');
expect(await loginPage.getDashboardTitle()).toBe('Dashboard');
Benefits: if the HTML changes, you update the page object, not dozens of tests. Tests are readable; they describe what the user does, not HTML selectors. Page objects can encapsulate complex interactions, reducing test code duplication.
Structure: one page object per page or major component. Methods represent user actions (login, fillForm). Getters return data for assertions. Selectors are private; only the page object knows them. This creates an abstraction layer.
Downsides: POM adds code. For simple projects with few tests, it’s overhead. For large test suites with hundreds of tests across many pages, POM is essential for maintainability.
How should data-driven testing be implemented?
Data-driven testing runs the same test logic with different data. Instead of writing one test per input, you write one test with parameterized data.
const testData = [
{ input: 2, expected: 4 },
{ input: 3, expected: 9 },
{ input: -1, expected: 1 }
];
testData.forEach(({ input, expected }) => {
test(`square of ${input} is ${expected}`, async () => {
expect(await calculator.square(input)).toBe(expected);
});
});
Benefits: reduces code duplication. You write the test logic once, then run it with many datasets. Tests are clearer about what data matters. Adding new test cases is easy; just add data.
Data sources can be CSV files, JSON, databases, or in-code arrays. For complex data, external sources are cleaner. For simple cases, in-code data is fine.
Challenges: error messages become less clear (which data point failed?). Debugging is harder; you need to know which iteration failed. Test execution time multiplies by the number of data points. Balance efficiency against maintainability.
Explain parallel test execution and its challenges.
Running tests in parallel reduces total execution time. With proper isolation, tests can run simultaneously on multiple machines or processes.
Challenges arise from shared state. If multiple tests write to the same database table, they interfere with each other. Parallel execution requires test isolation: each test needs its own data, its own test database instance, or shared data that’s protected by mechanisms like unique identifiers.
Strategies: shard tests (each shard runs a subset of tests on a different machine), isolate databases (each test gets a clean database), and use test fixtures that create independent data. Avoid global state; tests should not depend on execution order.
Test flakiness becomes more apparent with parallelization. If tests are flaky, parallelization might make flakiness worse due to resource contention. Before parallelizing, ensure tests are solid.
How should cross-browser testing be handled?
Cross-browser testing verifies that the application works across different browsers. With Selenium, you run the same test against Chrome, Firefox, Safari. With Playwright, you run against different browser engines.
Approaches: local testing (run tests locally against multiple browsers; slow but good for development), cloud services (BrowserStack, Sauce Labs; expensive but handle many browser versions and OS combinations), containerized browsers (Docker images with browsers; cheaper than cloud but less variety).
Strategy: test critical paths in multiple browsers; less critical functionality can be tested in just the primary browser. If 95% of users are on Chrome, focus on Chrome. But test Safari to ensure you’re not relying on Chrome-specific features.
Headless browsers (no UI) are faster than headed browsers. Use headless for speed, headed for debugging. CI typically uses headless; developers use headed.
What is headless testing and when should it be used?
Headless browsers run without a user interface. Headless Chrome or Firefox starts, but no window opens. Tests communicate with the browser via an API. Headless is faster because rendering the UI is skipped.
Benefits: speed (headless is 2-3x faster than headed), CI-friendly (no display needed), and reproducibility (consistent environment). Use headless in CI pipelines.
Downsides: some bugs only appear in headed browsers (display issues, animation timings). Debugging is harder without a visual interface. If a test fails in headless but passes in headed, investigating is tedious.
Best practice: develop tests with headed browsers for visibility and debugging. Run tests in headless in CI. If headless and headed differ, investigate; usually it indicates a real bug or a test that’s too sensitive to timing.
API Testing
How should REST APIs be tested at the service level?
Service-level testing sends HTTP requests and verifies responses. Libraries like Rest Assured (Java), pytest with requests (Python), or fetch (JavaScript) make it straightforward. Test the happy path (valid request returns expected response), error paths (invalid request returns appropriate error code), and edge cases (missing optional fields, null values).
test('GET /users/123 returns user', async () => {
const response = await fetch('/users/123');
expect(response.status).toBe(200);
const user = await response.json();
expect(user.id).toBe(123);
expect(user.email).toBeDefined();
});
Key assertions: HTTP status code, response headers, response body structure, and data types. Verify that response fields are present and correctly typed. Check that error responses include useful error messages.
Challenges: test data management (do you use a real database or mocks?), test isolation (multiple tests writing to the same database interfere), and side effects (a test creates a user; the next test sees it).
Solutions: separate test databases, factories that create test data cleanly, and cleanup (each test deletes what it created). Or mock external dependencies and test in isolation.
What is contract testing and how is it different from service testing?
Service testing verifies that an API works correctly in isolation. Contract testing verifies that the API conforms to the contract its consumers expect.
A consumer of an API records what it sends and what it expects back. These interactions form a contract. The provider (API) runs tests against the contract to ensure it satisfies the consumer’s expectations. If the API changes in a breaking way, contract tests fail.
Example: a payment service expects an order ID and returns a transaction. The order service records this contract. When the payment service team changes the response format, running the order service’s contract tests against the new payment service fails, catching the breaking change early.
Contract testing is powerful for preventing integration issues, especially with multiple services. It’s less useful for APIs with a single consumer where you control all clients. For a public API serving third parties, contract testing is essential.
How should API schema validation be tested?
Schema validation ensures responses match a declared schema. You define a JSON Schema, OpenAPI spec, or Protobuf schema, then test that API responses conform.
test('GET /users returns correct schema', async () => {
const response = await fetch('/users?limit=10');
const users = await response.json();
// Validate against schema
expect(users).toEqual(expect.arrayContaining([
expect.objectContaining({
id: expect.any(Number),
email: expect.any(String),
created_at: expect.any(String)
})
]));
});
Benefits: catches API changes that break clients. Enables client code generation from schemas. Schema tests are faster than full E2E tests.
Tools: JSON Schema libraries, OpenAPI/Swagger validators, and Protobufs built-in validation. Many API testing tools (Postman, Rest Assured) have schema validation built in.
How should authentication be handled in API tests?
Tests need credentials to make authenticated requests. Options: use a test account, generate tokens, or mock authentication.
Test account approach: use a static test user and login for each test. Simple but slow; each test incurs a login. Also, multiple tests might interfere with the same account’s state.
Token generation: if the API issues tokens, generate one at test setup and reuse it. Faster than logging in for each test but requires managing token expiration.
Mocking: skip authentication entirely by mocking the auth service. The API calls the mock, which returns “authenticated.” This is fastest but tests less of the real flow.
Best practice for integration testing: generate a token once at suite setup, use it for all tests, cleanup at the end. For E2E testing: login as a real user since the login flow is part of what you’re testing.
Never hardcode credentials in test code. Store them in environment variables or a secure config. In CI, use secrets management tools. Credentials in version control are a security nightmare.
How should asynchronous APIs be tested?
Async APIs process requests in the background. The API returns immediately with a request ID (202 Accepted). The client polls or receives a webhook when the work completes.
Testing approaches: polling (loop checking status until it’s done), webhooks (register a listener and wait for a callback), or explicit waits (wait up to a timeout).
test('async operation completes', async () => {
const response = await fetch('/process', { method: 'POST' });
expect(response.status).toBe(202);
const { request_id } = await response.json();
// Poll for completion
let status = 'pending';
const startTime = Date.now();
while (status === 'pending' && Date.now() - startTime < 10000) {
const checkResponse = await fetch(`/process/${request_id}`);
const data = await checkResponse.json();
status = data.status;
if (status === 'pending') await new Promise(resolve => setTimeout(resolve, 100));
}
expect(status).toBe('completed');
});
Challenges: tests take longer because you wait for async work. Polling introduces timing assumptions; you assume work completes within 10 seconds. If the system is slow, the test times out and appears to fail even though the work succeeded eventually.
Solutions: set reasonable timeouts based on system behavior, use exponential backoff in polling (wait 100ms, then 200ms, then 400ms) to reduce constant polling, and mock async work in unit tests to avoid waiting.
How should API performance testing be conducted?
API performance testing measures how fast the API responds and how many requests it can handle. Tools like k6, JMeter, and Locust let you send many concurrent requests and measure latency, throughput, and error rates.
Define a scenario: make 100 concurrent requests to the API, measure response time, check for errors. Increase load gradually: start with 10 concurrent requests, then 50, then 100. Watch for when performance degrades.
// k6 example
export default function () {
const response = http.get('https://api.example.com/users');
check(response, {
'status is 200': (r) => r.status === 200,
'response time < 200ms': (r) => r.timings.duration < 200,
});
}
Metrics to track: response time percentiles (p50, p95, p99 show distribution), throughput (requests per second), error rate (what percentage fail), and resource usage (CPU, memory on the API server).
Run performance tests regularly in CI or on a schedule. A baseline shows normal performance; deviations indicate regressions. Testing against production data is more realistic than synthetic data; see how the API behaves with real-world usage patterns.
Programming and Coding Questions
Write a test for a login function.
A good answer demonstrates knowledge of test structure, mocking, and assertions.
class LoginService {
constructor(userRepository, passwordHasher) {
this.userRepository = userRepository;
this.passwordHasher = passwordHasher;
}
login(email, password) {
const user = this.userRepository.findByEmail(email);
if (!user) throw new Error('User not found');
if (!this.passwordHasher.verify(password, user.passwordHash)) {
throw new Error('Invalid password');
}
return { userId: user.id, email: user.email };
}
}
// Test
describe('LoginService', () => {
let loginService;
let mockUserRepository;
let mockPasswordHasher;
beforeEach(() => {
mockUserRepository = {
findByEmail: jest.fn()
};
mockPasswordHasher = {
verify: jest.fn()
};
loginService = new LoginService(mockUserRepository, mockPasswordHasher);
});
test('login succeeds with correct credentials', () => {
const user = { id: 1, email: 'user@example.com', passwordHash: 'hash' };
mockUserRepository.findByEmail.mockReturnValue(user);
mockPasswordHasher.verify.mockReturnValue(true);
const result = loginService.login('user@example.com', 'password');
expect(result.userId).toBe(1);
expect(result.email).toBe('user@example.com');
expect(mockPasswordHasher.verify).toHaveBeenCalledWith('password', 'hash');
});
test('login fails with non-existent user', () => {
mockUserRepository.findByEmail.mockReturnValue(null);
expect(() => loginService.login('user@example.com', 'password'))
.toThrow('User not found');
});
test('login fails with incorrect password', () => {
const user = { id: 1, email: 'user@example.com', passwordHash: 'hash' };
mockUserRepository.findByEmail.mockReturnValue(user);
mockPasswordHasher.verify.mockReturnValue(false);
expect(() => loginService.login('user@example.com', 'wrong'))
.toThrow('Invalid password');
});
});
Discussion points: the test uses mocks to isolate the login logic from database and hashing. It tests the happy path (success) and error paths (user not found, wrong password). It asserts both the return value and that dependencies were called correctly.
Review this test code and identify bugs.
Interviewers present flawed test code and ask you to spot issues. Common problems:
Assertion too broad: `expect(result).toBeTruthy()` passes for any truthy value. Better: `expect(result).toEqual({ id: 1, name: 'Alice' })`.
Test state leakage: a test modifies global state and doesn't clean up. The next test sees the modified state. Solution: use beforeEach to reset state or use test doubles that don't share state.
Timing assumptions: a test sleeps for 100ms expecting async work to complete. If the system is slow, the test times out. Solution: explicit waits or callbacks, not fixed sleeps.
Mocks that are too loose: a mock returns any value. The test passes even if the code asks for the wrong thing. Solution: verify that the mock was called with the expected arguments.
Test doesn't test what it claims: a test named "test_login_with_invalid_email" tests valid email. Solution: test what the name promises.
Write a utility function for parsing test data from a CSV file.
A good answer shows understanding of file handling, parsing, and error handling.
const fs = require('fs');
const path = require('path');
function parseCSVTestData(filePath) {
if (!fs.existsSync(filePath)) {
throw new Error(`File not found: ${filePath}`);
}
const fileContent = fs.readFileSync(filePath, 'utf-8');
const lines = fileContent.split('\n').map(line => line.trim()).filter(line => line);
if (lines.length === 0) {
throw new Error('CSV file is empty');
}
const headers = lines[0].split(',').map(h => h.trim());
const data = [];
for (let i = 1; i < lines.length; i++) {
const values = lines[i].split(',').map(v => v.trim());
if (values.length !== headers.length) {
throw new Error(`Row ${i + 1} has ${values.length} columns, expected ${headers.length}`);
}
const row = {};
headers.forEach((header, idx) => {
row[header] = values[idx];
});
data.push(row);
}
return data;
}
// Example usage
const testData = parseCSVTestData('test-data.csv');
testData.forEach(row => {
test(`login with ${row.email}`, () => {
// test logic
});
});
This implementation handles file reading, parsing, error checking (file exists, correct column count), and returns structured data. A production version would handle quoted fields with commas, different encodings, and maybe use a CSV parsing library. For interviews, showing that you understand the problem and write careful code is what matters.
Describe a complex testing scenario you'd solve with code.
Interviewers want to see how you approach problems. A good answer describes a realistic challenge and your solution. Examples:
Scenario: testing a distributed system where services call each other. The challenge is that test order affects results; if Service A is tested before Service B, Service B's tests see calls from Service A's test. Solution: use service virtualization or contract testing to isolate services, or design tests so they clean up after themselves.
Scenario: testing a system with external dependencies (payment provider, email service). The challenge is that external calls are slow and unreliable. Solution: mock external services for most tests, use a test double that behaves realistically, and run a few integration tests against real services in a safe environment.
Scenario: large test suite that takes an hour to run. The challenge is slow feedback in development. Solution: parallelize tests, separate fast unit tests from slow E2E tests, and use test sharding to distribute tests across CI machines.
CI/CD and DevOps for Testers
How should tests be integrated into a CI/CD pipeline?
Tests are gated; they must pass before merging. Fast tests run first (unit tests in seconds). If unit tests pass, proceed to integration tests (minutes). If those pass, run E2E tests (tens of minutes). If all pass, the code is merged and deployed.
This ordering is important. Running E2E tests on every commit is too slow. Running no tests is too risky. The gate should provide confidence that new code doesn't break existing functionality.
Practical setup: unit and integration tests run on every PR. E2E tests run on main branch after merge. Nightly runs of comprehensive tests. Production monitoring catches issues that tests missed.
Parallel execution: distribute tests across multiple CI machines. With 10 machines, 100 tests that take 10 minutes serially run in 1 minute in parallel. CI providers (GitHub Actions, Jenkins, GitLab) support test sharding.
Artifact handling: CI saves test results, logs, and videos of failed tests. This helps debugging. Large artifacts should be cleaned up after a retention period to save storage.
What is test gating and how should it be implemented?
Test gating means tests must pass before code can be merged or deployed. A failing test blocks the merge. This prevents broken code from reaching main or production.
Implementation: set CI status checks on pull requests. A GitHub status check requires the test suite to pass. Only after passing can the PR be merged. For deployment gates, automated tests must pass in production before the release is considered successful.
The challenge is balancing strictness with productivity. Overly strict gating blocks legitimate changes on flaky tests. Loose gating lets broken code through. The solution is fixing flaky tests and giving them retries (if a test flakes, retry it automatically).
How should parallel test execution be set up in CI?
Most CI providers support parallel jobs. You define multiple jobs that run simultaneously. Each job runs a subset of tests.
// GitHub Actions example
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
shard: [1, 2, 3, 4]
steps:
- uses: actions/checkout@v2
- run: npm test -- --shard=${{ matrix.shard }}/4
This runs four jobs in parallel, each testing 1/4 of the test suite. Sharding strategies: by test file (job 1 runs tests in files 1-25, job 2 runs 26-50), by test tag (job 1 runs @smoke, job 2 runs @regression), or by time estimate (divide tests to balance execution time).
Challenges: test isolation (tests on different machines might interfere if they share data), reproducibility (flakiness might be hard to reproduce because parallel machines differ from local sequential execution), and debugging (which job failed?). Good logging helps; each test should output the shard it ran on.
What role do Docker and containers play in test environments?
Containers provide consistent test environments. A Dockerfile defines the environment (base image, dependencies, database). Running tests in Docker ensures the same environment on the developer's machine, CI, and staging.
Benefits: no "works on my machine" problems, easy reproduction of issues, and environment management without manual setup. A developer can run docker-compose up and have a test database running.
Limitations: container overhead slows tests. Running 100 tests in a container might take longer than on the host. For local development, running tests on the host might be faster; containers are more useful in CI.
Docker Compose orchestrates multiple services. Your app needs a database, cache, and message queue. docker-compose.yml defines all services. `docker-compose up` starts them all; tests point to localhost:5432 for the database.
How should test results and metrics be reported?
Test reports should be machine-readable (JSON, XML) and human-readable (HTML, dashboards). CI systems parse machine-readable formats and display results in the UI. Dashboards track trends over time: pass rate, average test duration, slowest tests.
Key metrics: total tests, passed, failed, skipped. Pass rate as a percentage. Average duration. Longest-running tests (candidates for optimization). Flaky tests (frequently fail and re-pass). Failure rate by test type (are E2E tests more flaky than unit tests?).
Reporting tools: standard CI output, Allure for beautiful reports, TestRail for test management, or custom dashboards. The goal is making it easy to spot trends and problems.
Alerting: if pass rate drops below 95%, notify the team. If test suite duration increases 20%, investigate. Alerts prevent slow degradation of quality.
Performance and Load Testing
Compare JMeter, k6, and Gatling for load testing.
JMeter is the oldest and most feature-rich. It's Java-based, has a GUI, and supports many protocols (HTTP, FTP, SOAP, JDBC). JMeter can be complex to set up but is powerful for intricate scenarios.
k6 is modern, lightweight, and developer-friendly. Tests are written in JavaScript. k6 runs locally and in the cloud (k6 Cloud). It's fast and good for quick load tests. k6 is less feature-rich than JMeter but easier to use.
Gatling is Scala-based and designed for continuous load testing in CI. It's very fast and generates beautiful reports. Gatling has a learning curve but is worth it for teams doing continuous performance testing.
Choose based on needs: for quick load tests, k6. For complex scenarios with many protocols, JMeter. For continuous CI-based testing, Gatling. In interviews, discussing your hands-on experience is better than guessing which is "best."
What metrics should be measured in performance testing?
Response time is the time from request to response. Measure percentiles: p50 (median), p95 (95% of requests are faster), p99 (99% of requests are faster). p99 matters because users in the slow 1% notice latency.
Throughput is requests per second. How many requests can the system handle? As load increases, throughput plateaus or decreases (the system is overloaded). Identify the breaking point.
Error rate is what percentage of requests fail. Errors might be legitimate failures (database full, exceeding rate limits) or bugs. At high load, error rates should increase gracefully, not spike to 50%.
Resource usage: CPU, memory, disk I/O. Is the API CPU-bound (CPU at 100% but memory fine), memory-bound, or I/O-bound? The bottleneck determines how to optimize.
Concurrent users: how many users can the system serve simultaneously? As you add users, latency increases. Find the inflection point where the system becomes unresponsive.
How do you identify bottlenecks in performance testing?
Start with a baseline: measure performance with a small number of users (10). Then gradually increase load and watch metrics. When does latency spike? When do errors appear?
Look at resource usage. If CPU is at 100%, the API is CPU-bound; optimize the code. If disk I/O is high and CPU is low, the API is I/O-bound; optimize queries or add caching. If memory is high, look for memory leaks.
Profiling tools show where time is spent. A CPU profiler samples the program and identifies hot functions. Does the API spend 50% of time in the database driver? Optimize queries. Does it spend 30% serializing JSON? Use a faster JSON library.
Database queries are often bottlenecks. Monitor slow queries (MySQL has slow query log, PostgreSQL has pg_stat_statements). Add indexes, optimize queries, or cache results.
After identifying the bottleneck, fix it and re-test. Performance improvements compound; fixing the database might reveal that the API is now CPU-bound, pointing to the next optimization.
Explain the difference between load, stress, and soak testing.
Load testing applies expected load. If you expect 1000 concurrent users, load test with 1000. Measure response time and error rates. Load testing verifies the system performs acceptably under expected conditions.
Stress testing exceeds expected load to find breaking points. If expected load is 1000 users, stress test with 5000. Watch for when the system fails: when do errors spike? When is latency unacceptable? Stress testing reveals limits.
Soak testing applies load over a long period to detect memory leaks and resource exhaustion. Run 100 concurrent users for 8 hours. Monitor memory; if it grows continuously, there's a leak. Soak testing simulates real-world sustained usage.
All three are important. Load testing verifies normal operation. Stress testing reveals limits. Soak testing catches long-term issues that wouldn't appear in a 10-minute test.
Mobile Testing
What is Appium and how is it used?
Appium is a framework for automating mobile apps (iOS and Android). It's similar to Selenium but for mobile. Tests are written in the same way as web tests: find elements, click them, verify results.
Appium works by communicating with the mobile OS via WebDriver protocol. On Android, Appium uses UiAutomator or Espresso. On iOS, it uses XCUITest. Tests run on real devices or emulators.
Benefits: write tests once, run on multiple platforms. Use the same test code for Android and iOS (with minor platform-specific handling). Appium is open-source and widely supported.
Challenges: setting up Appium is complex (Xcode, Android SDK, dependencies). Tests are slower than web tests. Flakiness is common due to app state and timing issues. Cross-device testing is necessary; behavior differs between devices.
Compare testing on real devices vs emulators.
Real devices are authentic. You test on the actual hardware your users use. Real devices catch hardware-specific bugs and performance issues. The challenge is management: you need a farm of devices, they break, and they're expensive.
Emulators are simulated devices. Android emulator is a virtual machine running Android. iOS simulator simulates iOS on a Mac. Emulators are cheap (just software) and easy to manage. The challenge is that emulators don't perfectly mimic hardware; some bugs only appear on real devices.
Best practice: develop and test on emulators for speed. Run smoke tests on real devices to catch hardware-specific issues. Cloud services like BrowserStack provide real device farms; you rent access instead of owning devices.
How should Android and iOS testing differ?
Android uses Espresso (UI testing framework), JUnit (unit testing), Robolectric (fast unit tests without an emulator), and Calabash (cross-platform). Android apps are built with Java/Kotlin.
iOS uses XCTest (native testing framework), XCUITest (UI testing), and Calabash. iOS apps are built with Swift/Objective-C.
Conceptually, testing is similar across platforms: find elements, perform actions, verify results. Practically, each platform has unique idioms. An Android app might use intents for navigation; an iOS app uses view controllers. Tests must understand the platform.
For cross-platform apps (Flutter, React Native), tests are more consistent. You write tests once for both platforms. But you still need to test platform-specific behavior (iOS notification handling differs from Android).
Behavioral and Process Questions
Describe how you approach testing a new feature from scratch.
Start by understanding the feature. Read the requirements, look at mockups, and ask clarifying questions. What are success criteria? What edge cases matter? What if something fails?
Design the test strategy. What needs unit tests (core logic)? What needs integration tests (data flow)? What needs E2E tests (user workflow)? Estimate effort and prioritize.
Write tests for the happy path first, then error paths, then edge cases. For a login feature: valid credentials (happy), wrong password (error), user not found (error), SQL injection (edge), extremely long password (edge).
Collaborate with developers. Ask about implementation details to inform test design. Get early access to code to write tests during development, not after. Suggest test infrastructure (fixtures, utilities) that make tests easier.
Iterate. As developers implement, run tests and provide feedback. If a test fails, is it a bug in the code or a bad test? Be precise in bug reports; include reproduction steps.
How do you advocate for quality when release pressure is high?
Present data. "We have 10 failing tests and 5 flaky tests. The failure rate is 3%. Here's what we know is broken." Numbers are more persuasive than "I don't think we should release."
Rank risks. Not all bugs matter equally. A payment processing bug is critical; a typo in error text is not. Focus on critical bugs. If critical bugs are fixed, releasing is acceptable even if non-critical issues remain.
Suggest mitigations. If you can't delay release, offer alternatives: release to a subset of users first, monitor closely for issues, have a rollback plan. Be a partner in shipping, not just a blocker.
Build trust by being accurate. If you say a feature isn't ready, be right. If you say it's ready, be right. Developers remember SDETs who boy-cry wolf.
How do you work with developers who don't think testing is important?
Lead by example. Write tests that find bugs. Show the business impact of bugs (customer complaints, support costs, reputation). Help developers understand that testing saves them time in the long run.
Make testing easy. If tests are painful to write, developers avoid them. Provide utilities, fixtures, and documentation. Make it clear how to write good tests.
Involve developers early. Ask them to review test design. Get their input on what's testable and what's brittle. Ownership makes people care.
Celebrate wins. When a test catches a bug before release, make sure the developer knows they're using the test infrastructure correctly.
Describe a situation where you dealt with unclear or incomplete requirements.
A good answer shows pragmatism and communication. Example: you're asked to test a feature but the requirements are vague. You could wait for clarity (slow) or make assumptions (risky).
Best approach: ask clarifying questions. "What happens if the user submits an empty form? What if they submit invalid data? What's the success criteria?" Get answers, document them, share with the team. This prevents misunderstandings.
If answers aren't available, make reasonable assumptions and document them. "I'm assuming we reject empty submissions with error X because that matches the pattern in feature Y." Share these assumptions with developers. They'll correct you if needed.
Adapt as you learn. As features are implemented, your understanding deepens. Update tests accordingly. Testing is not a waterfall process; it's iterative.
How do you handle flaky tests that affect release timelines?
First, determine if the test is actually flaky or if there's a real bug. Rerun the test multiple times. If it passes sometimes and fails sometimes with no code changes, it's flaky.
Don't ignore flaky tests or disable them. Every flaky test is debt. It erodes trust and hides real bugs. But during a time-sensitive release, you might have to temporarily disable it with a comment: "TODO: fix flaky test in X.test.js; tracking issue #123."
After release, prioritize fixing it. Identify the root cause (timing, external dependencies, test isolation). Fix it properly, not with band-aids like increasing timeouts. Rerun the test 100+ times to verify it's stable.
Describe your experience with testing in a CI/CD environment.
Share specific examples. "We run 2000+ tests in CI. Unit tests finish in 2 minutes, integration tests in 5 minutes, E2E tests in 15 minutes. Tests are sharded across 10 machines. Failed tests block merge. We have a dashboard showing test trends over time."
Discuss challenges you've solved. "Tests were flaky due to timing issues; we switched from sleep(1000) to explicit waits. Test execution time was growing; we added mocking and reduced E2E test scope."
Discuss infrastructure. "We use GitHub Actions for CI, store test artifacts for 30 days, and have alerts if pass rate drops below 95%."
Questions to Ask the Interviewer
What does the current test suite look like? How many tests, how long to run, what's the coverage?
What are the main pain points with testing right now?
How is testing currently integrated into the development process?
What tools and frameworks is the team currently using?
How much time do developers spend writing tests vs features?
What's the team's appetite for improving testing infrastructure?
How do you handle test failures in CI?
What is shift-left testing in the context of SDETs?
Shift-left testing means moving testing earlier in the development lifecycle. Traditionally, SDETs would receive completed features to test. Shift-left involves SDETs in design phases, helping architects understand testability implications before code is written. An SDET might say, "If you make this component internal, we can't test it from outside. Consider making it injectable for testing." This prevents hard-to-test designs before they're built.
Practically, shift-left means SDETs pair with developers during implementation. You write tests as features are built, not after. You provide feedback on API design, error handling, and logging. A well-logged system is easier to test; you can verify internal state through logs without breaking encapsulation.
The impact is significant: bugs are caught earlier and cost far less to fix. A design issue caught during implementation takes hours to fix. The same issue found in production takes days and affects customers. Shift-left is about preventing problems, not just finding them.
What role does monitoring and observability play in test automation?
Observability is the ability to understand system state from external outputs. For testing, observability means you can verify behavior through logs, metrics, and traces without modifying the code. An SDET designing tests needs to ensure the system is observable.
Without observability, you're limited to testing external behavior: check the HTTP response. With observability, you can verify internal behavior: was the cache hit? Was the database query optimized? Did the payment processor get called with the correct data? Metrics and structured logging make this possible.
As an SDET, you might advocate for better logging. "We need to log when a cache miss happens so we can verify cache behavior in tests." Or metrics: "Can we emit a metric when a payment succeeds? So we can verify the entire flow in production monitoring."
This connects testing to production. Tests verify behavior in controlled environments; monitoring verifies behavior in production. Both use the same data sources (logs, metrics). An SDET who thinks about observability designs tests that translate to production monitoring.
Advanced Testing Patterns
What is property-based testing and when should it be used?
Property-based testing generates random inputs and verifies that certain properties always hold. A property is an invariant: something that should be true for all inputs. For a sort function, a property is "the output is sorted." You generate hundreds of random lists, sort them, and verify they're all sorted.
Property-based testing is powerful because it explores input space far beyond what manual test cases do. You might manually test a list of 10 items, but property-based testing tests lists of 0, 1, 1000, and 1 million items. It tests edge cases you wouldn't think of.
Libraries like QuickCheck (Haskell), Hypothesis (Python), and jsverify (JavaScript) implement property-based testing. You define the property and the input generator. The framework does the rest.
Use property-based testing for mathematical operations, data transformations, and parsing. Don't use it for stateful operations or operations with complex dependencies. The property must be simple enough to express concisely.
What is mutation testing and how does it improve test quality?
Mutation testing modifies the code under test and checks if tests catch the modification. A mutation might change a > to >=, remove a line, or negate a condition. If tests don't catch the mutation, you have weak tests that don't adequately verify the code.
Example: a function checks if a number is positive. Your tests might verify that 5 returns true. But if you mutate the code to always return true, tests still pass. A good test would verify both that 5 returns true and that -5 returns false.
Mutation testing reveals gaps in test coverage that code coverage misses. 90% code coverage with weak assertions is worse than 70% code coverage with strong assertions. Mutation testing quantifies assertion strength.
Tools like Stryker (JavaScript), PIT (Java), and mutmut (Python) automate mutation testing. Run them in CI to catch weak tests. They're slow (they run tests many times), so use them selectively or on a schedule.
How should you approach testing error scenarios and edge cases?
Error scenarios are the most important and most neglected part of testing. Happy path tests verify the system works when everything goes right. Error scenarios verify the system fails gracefully when things go wrong.
Categories: input errors (null, empty, too large, invalid format), state errors (resource doesn't exist, operation not allowed in current state), and system errors (database down, external service unavailable, out of memory).
For input errors, test boundary values. If a field accepts 1-100, test 0, 1, 100, and 101. Test null, empty strings, and extremely long strings. Test invalid formats: for an email field, test "not-an-email" and special characters.
For state errors, test operations in invalid states. A purchase API shouldn't allow refunding a non-existent order. An auth API shouldn't allow confirming an already-confirmed email. Write tests that verify the correct error is returned.
For system errors, test graceful degradation. If a cache is down, does the system fall back to the database? If an external API is slow, does the request timeout gracefully? Mock dependencies to fail and verify the system handles it.
Soft Skills for SDETs
How do you communicate test results to non-technical stakeholders?
Developers understand that a test failure means something is broken. Non-technical stakeholders don't. You need to translate test results into business impact.
Instead of: "3 E2E tests failed, 12 integration tests failed, 5 unit tests failed," say: "We found 3 critical bugs that affect the payment flow, 12 issues in data processing, and 5 low-level bugs that are likely side effects of the critical issues."
Include impact assessment: "The critical bugs prevent users from completing purchases. We estimate they'd affect 5-10% of users if released." This gives stakeholders information to make release decisions.
Use dashboards showing trends: "Our test pass rate has been 98% for the last month. Last week it dropped to 94% due to a new feature. We've fixed most issues; we're back to 97% today." Trends are more meaningful than snapshots.
How do you balance speed and coverage when designing tests?
Every test you write slows down CI. A test suite that takes 2 hours to run gives feedback so slowly that developers stop waiting for results and merge without checking. A test suite that finishes in 5 minutes is too fast to be comprehensive.
The balance depends on your team. A startup where speed is critical might accept lower coverage for faster feedback. An enterprise where quality is paramount might accept slower feedback for comprehensive coverage.
Practically: run fast tests (unit tests) on every commit. Run medium tests (integration) on every PR. Run slow tests (E2E) only on main branch before deploy. This gives developers fast feedback while still being thorough.
Another strategy: run all tests in CI but parallelize aggressively. 2 hours of tests across 20 machines is 6 minutes of wall clock time. The investment in parallelization pays off quickly.
How do you stay current with testing tools and practices?
Testing is an active field. New tools, frameworks, and methodologies appear regularly. Staying current is important for an SDET.
Read blogs from testing leaders (Test Automation University, Ministry of Testing), follow GitHub for new frameworks, and experiment on small projects. When a new tool emerges, spend a weekend learning it. If it's useful, introduce it to your team gradually.
Attend conferences (TestBash, Selenium Conference) and webinars. Join communities like the Selenium Group or Cypress Discord. Learning from peers who face similar problems is valuable.
Most importantly, learn from your own testing. When tests fail mysteriously, investigate. When you write the same test utility three times, abstract it. Let your experience guide learning, not hype.
What is risk-based testing and how does it inform test strategy?
Risk-based testing prioritizes tests based on the likelihood and impact of failure. High-risk areas get more testing; low-risk areas get less. This is pragmatic: you have limited testing time, so focus on what matters most.
Risk = probability of failure * impact of failure. A payment processing bug has high impact (customers can't complete purchases) and reasonable probability (complexity and stakes are high), so it's high-risk and needs comprehensive testing. A typo in an error message is low-risk: low probability of bugs and low impact.
Assess risk by considering: complexity (complex code has more bugs), criticality (is it on the critical path?), change frequency (recently changed code is riskier), and technology novelty (new frameworks are riskier than proven ones).
As an SDET, you guide risk assessment. "The authentication system is high-risk because it's complex and critical. Let's allocate 30% of our testing time there." This conversation with developers ensures testing effort matches actual risk, not just perceived importance.
How do you approach testing distributed systems and microservices?
Distributed systems introduce complexity: network delays, partial failures, eventual consistency. A request succeeds on the client side but fails on the server. A service goes down mid-transaction. Tests must account for these scenarios.
Testing strategies: stub external services (mock them to fail sometimes), test timeout and retry logic, verify idempotency (the same request processed twice produces the same result), and test cascading failures (if Service A depends on Service B, what happens when B fails?)
Contract testing is especially valuable for microservices. Each service team owns their service but must honor contracts with other services. Contract tests catch integration issues early without requiring full end-to-end environments.
Consider testing in a realistic staging environment where multiple services run together. Local unit tests miss integration issues. A staging environment that mirrors production catches real-world problems: network latency, service startup order, shared databases.
What metrics and KPIs should you track for a test automation program?
Metrics for test execution: total tests, pass rate, flaky test rate, test execution time, coverage (code and feature). These show the program's health.
Metrics for bugs caught: bugs found by test automation, bugs escaped to production, time to fix bugs. These show the program's value. If automation finds bugs and prevents them from reaching production, it's working.
Metrics for team productivity: time spent writing tests vs features, time spent maintaining tests, velocity (features shipped per sprint). Tests are overhead if they slow the team; they're valuable if they enable faster, safer shipping.
Metrics for quality: customer-reported bugs, support tickets related to bugs, uptime, performance metrics. These show whether testing translates to product quality.
Present metrics regularly and react to trends. If flaky tests increase, investigate and fix. If bugs are escaping to production, assess whether testing is sufficient or if risks have shifted. Metrics aren't just dashboards; they're signals for action.
How do you mentor other engineers in test automation?
SDETs are often the experts in testing on their teams. You'll be asked to mentor developers on writing testable code and tests.
Start with principles, not tools. Teach test independence, isolation, and clarity. Show how parameterized tests reduce duplication. Explain the pyramid. Once principles sink in, tools are easy.
Code review is a mentoring tool. When a developer writes a test, review it thoroughly. Point out: "This test is checking three things; split it into three tests." "This test depends on test order; add setup so it runs independently." "This assertion is too broad; be specific."
Pair programming accelerates learning. Sit with a developer and write tests together. Explain your thought process. Let them drive while you navigate. They learn by seeing and doing, not by being told.
Lead by example. Write tests that are clear, maintainable, and comprehensive. When you refactor a test to be clearer, point it out. Share articles or tools you find useful. Enthusiasm for testing is contagious.
What are antipatterns in test automation to avoid?
Test antipatterns are patterns that seem good but cause problems. Recognizing them prevents wasted effort.
The assertion assertion: tests that assert on test infrastructure instead of the code. "Verify that the mock was called" when you should verify the code's output. This creates tests that pass when the code is wrong.
The test implementation: writing tests that are as complex as the code under test. If tests are hard to understand, they're not testing; they're just more code to maintain. Tests should be simple and clear.
The brittle test: tests that break whenever implementation details change, even if behavior doesn't. Avoid testing internal state; test observable behavior. Tests should be resilient to implementation changes.
The slow test: tests that could be unit tests but are E2E tests because the developer took a shortcut. A test that could run in 10ms but takes 10 seconds because it starts a server is a slow test. These accumulate and kill feedback speed.
The coupled test: tests that depend on order or shared state. If you run a single test in isolation, it fails. If you run tests in a different order, they fail. This is chaos; fix it immediately.
Recognize these antipatterns in code review and refactor. An hour fixing antipatterns early saves days of frustration later.
See our comprehensive guide to the best answers to interview questions for more insights. Related resources include our guides to Kubernetes interview questions, Kafka interview questions, Terraform interview questions, Snowflake interview questions, quality engineer interview questions, data analyst interview questions, and Glassdoor interview questions.

Leave a Reply