Testing Strategy at Scale Interview Questions | JiQuest

add

#

Testing Strategy at Scale

Advanced module · testing & quality engineering

Testing strategy at scale: the pyramid, contracts, mutation testing, and staying non-flaky.

A large test suite doesn't fail from having too few tests — it fails from having the wrong shape: too many slow end-to-end tests, coverage numbers that hide weak assertions, and a growing pile of flaky tests nobody trusts. This is the reasoning senior engineers actually use to keep a suite fast, honest, and worth believing.

3Pyramid layers
1Ice-cream-cone anti-pattern
0Trust in a flaky red build
E2E few, slow Integration Testcontainers, contracts Unit tests fast, cheap, numerous the shape is the strategy, not the total count

Why "just write more tests" is not a strategy

Every backend team accumulates tests over time, but very few teams deliberately design the shape of their suite. The result, almost always by accident, is a pile of slow end-to-end tests that catch bugs late, a thin layer of unit tests that don't actually assert on behavior, and a rotating cast of flaky tests everyone has learned to re-run instead of trust. A real testing strategy answers a small number of concrete questions: what layer should this specific bug have been caught at, how do we know our tests would actually catch it, and what does it cost in CI minutes to find out.

Speed of feedbackA developer should learn about a regression in minutes on their own change, not hours later in a shared integration environment.
Trust in a red buildA failing test has to mean something broke — the moment flaky failures become normal, red stops being a signal at all.
Coverage of actual behaviorExecuting a line is not the same as verifying its outcome; the suite has to prove it would catch a real bug, not just that it ran.
Interface stability across teamsTwo independently deployed services need a way to know they still agree on an API shape without running each other's full stack.

Jump to a section

The test pyramid: matching test type to what it actually costs

The pyramid isn't a rule about proportions for their own sake — it's a direct consequence of what each layer costs to run and how much of the system it has to stand up to get a signal. A unit test needs nothing but the class under test and maybe a couple of collaborators, so it's cheap enough to write hundreds of and run on every keystroke. An end-to-end test needs a deployed, wired-together system, so by comparison it is slow, expensive to maintain, and touches so much surface area that a failure rarely points at the actual cause.

End-to-end deployed system, UI/API level Integration real DB/broker via Testcontainers, contract tests Unit pure logic, no I/O, milliseconds each slowest, most brittle, clearest business-outcome proof catches real-dependency bugs a mock structurally cannot fastest, most numerous, pinpoints the exact broken function
LayerWhat it needs runningTypical speedWhat a failure tells you
UnitNothing external — the class under test, in-processMillisecondsExactly which function or branch broke
IntegrationA real dependency (Postgres, Redis, Kafka) via Testcontainers, or a verified contractSecondsThe component's boundary with the outside world is wrong
End-to-endA deployed, wired-together system across servicesMinutesSomething in the whole flow broke — needs further digging to localize
The rule of thumb, and why it's a rule of thumb Push a test as far down the pyramid as it can go while still proving what it needs to prove. If a unit test with a well-chosen fake can verify the logic, write that instead of an integration test; if an integration test against a real dependency can verify a boundary, write that instead of a full end-to-end test. The pyramid shape isn't imposed from outside — it falls out naturally once every test is written at the cheapest layer that can actually prove its claim.

The ice-cream-cone anti-pattern

Flip the pyramid upside down — a huge layer of end-to-end tests, a thin middle of integration tests, and almost no unit tests — and you get the "ice cream cone," a well-known anti-pattern that shows up gradually, not by anyone's explicit decision.

How teams end up here without meaning to

E2E tests feel more "real"Testing through the actual API from the outside feels like it proves more than a unit test of one method, so it's an intuitive first instinct even though it costs far more per bug caught.
Unit tests get skipped under deadline pressureWriting a fast, focused unit test forces you to actually think through a function's edge cases; an E2E happy-path test can be added quickly without that thinking, so it's tempting to default to.
Legacy code resists unit testingCode with hidden dependencies and static calls is often only testable by standing up the whole system, which pushes coverage toward the E2E layer by necessity rather than choice.
Nobody owns the suite's shapeTest count and pass rate get watched; the ratio between layers rarely does, so the inversion happens slowly enough that no single change looks like the cause.
What it costs once it's entrenched CI times climb toward an hour, failures become expensive to localize because a broken end-to-end flow could be caused by any of a dozen services, and the team's response to a slow, unreliable suite is almost always to run it less often or skip it under pressure — which quietly defeats the entire purpose of having automated tests in the first place.

The fix is rarely "delete the E2E tests" — a handful of them genuinely earn their keep, proving the deployed system works end to end. The fix is asking, for every existing E2E test, whether the same defect could have been caught by a cheaper integration or unit test instead, and migrating coverage downward wherever the answer is yes.

Consumer-driven contract testing

Once a system has more than a couple of independently deployed services, full integration tests between every consumer and every producer stop being practical — you'd need each producer's real, running system available in every consumer's CI pipeline, which turns into either a fragile shared environment or a combinatorial explosion of test infrastructure. Contract testing solves a narrower but much more tractable problem: does the consumer and producer still agree on the shape of the API between them, without either one needing the other's full running system.

Consumer test suiteasserts expected request/response Contract filepact.json / Groovy DSL Contract brokershared, versioned store Producer CIits own pipeline generate publish fetch Producer's real codereplays each interaction verify against contract pass or failproducer build gate a failed verification blocks the producer's deploy — it broke a real consumer's expectation Neither side ever runs the other's full deployed system in CI — the contract file is the only thing that crosses the boundary.

Why "consumer-driven"

The consumer, not the producer, defines what it actually needs from an interaction — which fields it reads, which status codes it branches on — and that recorded expectation becomes the contract the producer is held to. This inverts the usual failure mode where a producer changes its API based on what it assumes consumers use, breaking a consumer that depended on a field the producer didn't realize mattered. Spring Cloud Contract takes a related but producer-driven approach: the producer defines the contract and generates both a verification test for itself and a WireMock stub consumers can run against locally, which suits teams where the producer is the natural owner of the API shape.

ScenarioIntegration testContract test
Producer renames a field the consumer readsCatches it, but only if both services happen to be running together in the test environment at the moment of the changeCatches it in the producer's own CI, the moment the producer's code stops matching the contract
Need to test 12 consumers against 1 producerNeeds all 12 consumers' and the producer's running systems available togetherProducer verifies against 12 independently-published contracts, none of which require a running consumer
Proving the real network call works end to end under loadThis is what integration/E2E tests are for — contracts don't cover thisOut of scope — contracts only prove interface agreement, not runtime behavior under load

Testcontainers patterns: testing against the real thing

Both the payment ledger and the rate limiter mini projects on this site lean on the same underlying idea: a test that talks to a real, disposable Postgres or Redis container catches classes of bugs that a mocked repository or an embedded in-memory fake structurally cannot. That's not specific to those two projects — it's a general principle worth generalizing on its own.

What a mock can never catch

Real constraint violationsA mocked repository returns whatever you told it to; a real Postgres enforces the unique index, foreign key, and check constraint exactly as production will, including the exact exception type and message your error-handling code depends on.
Real locking and concurrency behaviorThe payment project's SELECT ... FOR UPDATE ordered-locking design can only be proven correct by firing genuinely concurrent transactions at a real database engine — a mock has no lock manager to get right or wrong.
Real serialization quirksJSON column round-tripping, timestamp timezone handling, and numeric precision (a DECIMAL column vs. a Java BigDecimal) all depend on the actual driver and engine, not on an assumption baked into a mock's stub.
Real atomic script behaviorThe rate limiter's Redis Lua script is atomic because Redis guarantees it; an in-memory fake of Redis has no such guarantee unless someone carefully re-implements it, which is itself a source of subtle divergence from production.

The pattern, generalized

// Any module with a real external dependency follows the same shape:
@Testcontainers
@SpringBootTest
class OrderRepositoryIT {

    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16");

    @DynamicPropertySource
    static void datasourceProps(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", postgres::getJdbcUrl);
        registry.add("spring.datasource.username", postgres::getUsername);
        registry.add("spring.datasource.password", postgres::getPassword);
    }

    @Test
    void savingADuplicateOrderNumberViolatesTheRealUniqueConstraint() {
        orderRepository.save(new Order("ORD-1", ...));

        assertThatThrownBy(() -> orderRepository.saveAndFlush(new Order("ORD-1", ...)))
            .isInstanceOf(DataIntegrityViolationException.class);   // a mock would never surface this
    }
}
The container is disposable, the assertion is what matters Testcontainers starts a fresh, throwaway instance per test class (or per test, if isolation demands it) and tears it down afterward — nothing about this requires a shared, long-lived test database that other tests or other developers can pollute. The point isn't "use real infrastructure everywhere"; it's "when a test's whole purpose is to verify behavior that only the real dependency actually has, don't fake the one thing the test exists to check."
Real Postgres via TestcontainersReal Redis via TestcontainersReal Kafka via TestcontainersDisposable per test class

Mutation testing: measuring whether your tests would actually catch a bug

Line and branch coverage answer "did my tests execute this code," which is a necessary but shallow question. Mutation testing answers the question that actually matters: "if this code had a bug, would my tests notice?" A mutation testing tool (PIT is the standard choice on the JVM) automatically generates many small, deliberate bugs — mutants — by rewriting the compiled bytecode, then reruns your existing test suite against each one and reports what fraction of mutants caused a test to fail.

What a mutant actually looks like

Conditional boundary>= flipped to >, off-by-one in a loop or comparison
Negated conditionalif (isValid) flipped to if (!isValid)
Return valueA method that returns true is mutated to always return false
Removed void callA statement like payer.setBalance(...) is deleted entirely

A 100%-covered test that mutation testing would expose

// 100% line coverage, and a completely useless assertion
@Test
void transferSucceeds() {
    assertDoesNotThrow(() ->
        transferService.transfer("key-1", payerId, payeeId, BigDecimal.TEN));
    // every line inside transfer() executed -- coverage tools mark this green.
    // but nothing here checks the RESULT of the transfer.
}

// A mutant that flips payer.subtract(amount) to payer.add(amount)
// still passes this test, because no assertion would ever notice.
// PIT reports this mutant as SURVIVED -- the exact signal that the test is weak.

@Test
void transferMovesTheExactAmountBetweenBothWallets() {
    transferService.transfer("key-1", payerId, payeeId, BigDecimal.TEN);

    assertThat(walletRepository.findById(payerId).getBalance()).isEqualByComparingTo("990");
    assertThat(walletRepository.findById(payeeId).getBalance()).isEqualByComparingTo("10");
    // the same mutant now KILLS this test -- 980 or 1010 fails the assertion immediately.
}
Why this matters more than the coverage number The first test above contributes to a coverage report exactly as much as the second one — both execute the same lines. Only mutation testing distinguishes them, because only mutation testing actually checks whether a real defect in that code would be caught. A codebase can carry 95% line coverage and still be riddled with tests shaped like the first example.

Where it fits in a real pipeline

Mutation testing is expensive — rerunning a test suite once per mutant multiplies your test runtime by however many mutants get generated, which is not something you want gating every commit. Most teams run it on a schedule (nightly, or weekly) across the whole codebase, or scoped to a single module under active review, and treat a low mutation score on a module as a prompt to strengthen that module's assertions, not as a per-commit CI gate the way the test suite itself is.

Chaos testing for correctness

This is deliberately narrower than the chaos engineering covered in the SRE and incident management module, which is about operational resilience in a live or staging environment — killing pods, saturating network links, verifying the system as a whole survives. Chaos testing for correctness lives inside an automated integration test and has a much more specific job: prove that a particular retry, fallback, timeout, or circuit-breaker branch of your code actually executes and produces the right result, not just that the system doesn't fall over.

Why the happy path isn't enough

A fallback branch is frequently the least-exercised code in an entire service. In production it only runs during the rare window when a real dependency is genuinely degraded, which means a typo in the fallback's field mapping, an exception silently swallowed instead of logged, or a stale assumption about the downstream error format can sit completely undetected for months — right up until the one incident where that exact branch was needed and it didn't work.

Inject a timeoutPoint the client at a WireMock stub configured with a fixed delay past your configured timeout, or pause a Testcontainers container mid-call, and assert the circuit breaker actually opens.
Inject a partial or malformed responseStub a truncated JSON body or an unexpected status code and assert the deserialization failure is handled, not an uncaught exception that bubbles up as a 500.
Inject a dropped connectionA Toxiproxy container placed in front of a real dependency can reset the connection mid-request on demand, proving the retry logic actually retries rather than just being present in the code.
Kill the dependency outrightStop the Testcontainers container entirely mid-test (the rate limiter project's fail-open path is tested exactly this way) and assert the documented degraded behavior, not a hang or a crash.
@Test
void whenRedisBecomesUnavailableTheLimiterFailsOpenInsteadOfBlockingRequests() {
    redisContainer.stop();   // deliberately kill the real dependency mid-test

    boolean allowed = rateLimiter.tryConsume("test-key", "route:/orders");

    assertThat(allowed).isTrue();   // fail-open: an outage must not become a global outage
    assertThat(meterRegistry.get("ratelimiter.fail_open").counter().count()).isEqualTo(1.0);
}
The point isn't to make tests flaky on purpose Chaos-injected tests target one specific, deterministic failure condition (a killed container, a stubbed timeout) rather than relying on genuinely random real-world timing, so they stay reproducible and non-flaky even though they're exercising a failure path. That determinism is what separates this from the network-flake category covered in the next section.

Flaky test triage

A flaky test is one that passes and fails against the same code, for no code-related reason. The temptation is to treat every flake the same way — re-run it until it's green — but that erases the diagnostic information a flake is actually giving you. Different root causes need different fixes, and misdiagnosing one as another usually means the same test keeps flaking for months.

Test fails intermittently reproduce how? Passes alone,fails in the full suite Fails only underparallel execution Fails more underload or slow CI runners Fails correlated witha third party's status Test-orderingdependency on state leftby a prior test Shared mutable statea static field or sharedfixture, not thread-safe Real timing/concurrencyrace — a genuine bug inasync or threaded code External network callan un-isolated dependencythe test should have stubbed Fix: isolate fixtures / fix the ordering the fix differs by branch, but every branch quarantines first, fixes second — never just re-runs and moves on

Quarantine is not the same as ignoring

Quarantining a flaky test means pulling it out of the blocking CI gate so it stops failing unrelated pull requests, while keeping it running and its failures visible, typically with an assigned owner and a deadline. Deleting the test, or silently marking it @Disabled with no tracking, removes whatever coverage it provided with no record that the gap exists — the bug that test used to catch is now completely unguarded, and nobody is accountable for noticing.

Root causeHow it typically reproducesTypical fix
Test-ordering dependencyPasses run alone, fails in the full suite or a different run orderEach test creates and tears down its own fixtures; never rely on state a prior test happened to leave
Shared mutable stateFails specifically under parallel executionMove shared state to per-test instances, or make it properly thread-safe
Real timing/concurrency raceFails more often under load or on a slower CI runnerReplace a fixed sleep with an explicit wait-for-condition; fix the actual race in the code under test if the race is real
External network callFailures correlate with the third party's own incidents, not your code changesStub the call in unit tests; isolate the real call to a small, clearly-labeled integration layer

CI test optimization: parallelization, sharding, and the cost trade-off

Once the suite's shape is healthy and flakiness is under control, the remaining lever is wall-clock time, and the standard answer is parallelization — splitting the suite across multiple workers (shards) that run concurrently. The trade-off is straightforward to state and easy to get wrong in practice: more shards buys faster feedback, but each shard has fixed overhead (spinning up a runner, starting containers, JVM warm-up) that doesn't shrink, and every additional shard is additional compute spend.

Diminishing returns past a pointDoubling shards roughly halves runtime only while per-test work dominates; once fixed per-shard startup cost is a large fraction of the shard's total time, more shards stop helping much.
Sharding can surface hidden ordering bugsSplitting a suite across shards separates tests that used to run in the same process, which is a common way a pre-existing test-ordering dependency first gets noticed.
Balance the shards, not just the countSplitting by test count instead of historical duration leaves one shard with all the slow Testcontainers-backed tests as the bottleneck, defeating the parallelization.
Pick a target, not a maximumMost teams pick the smallest shard count that gets pull-request feedback under a target (5–10 minutes is common), rather than maximizing parallelism for its own sake and paying for compute nobody needed.
# CI config sketch: shard by historical test duration, not test count
jobs:
  test:
    strategy:
      matrix:
        shard: [1, 2, 3, 4]
    steps:
      - run: ./gradlew test --tests-shard=${{ matrix.shard }}/4 --split-by=duration
      # unit tests spread evenly across all 4 shards; the handful of Testcontainers-backed
      # integration tests are pinned to specific shards so no shard becomes the bottleneck
Speed and cost are the same lever, pointed in opposite directions There's no shard count that's free — every shard is a parallel compute unit billed the same as running the suite serially, just concurrently. The right number is the one where marginal feedback-speed gained per additional shard is still worth the marginal CI spend, which is a decision to make deliberately and revisit as the suite grows, not a one-time default.

Key design decisions and interview talking points

Why does the test pyramid put unit tests at the base instead of splitting effort evenly across all three layers?

Cost and speed scale very unevenly across the layers — a unit test runs in milliseconds with nothing external, while an end-to-end test needs a deployed, wired-together system and runs in minutes. If you want a suite that's cheap enough to run on every commit and still trustworthy, the fast and cheap layer has to be the largest one, and the slow, expensive, failure-prone layer has to stay deliberately small.

What actually goes wrong when a team ends up with the ice-cream-cone anti-pattern?

CI slows to the point where a pull request takes 40+ minutes for a signal, and that signal is noisy because a failure could be a real regression, a flaky network call, a timing race, or a stale shared-environment fixture, with no fast way to tell which. Teams respond by re-running failed builds until they go green, which quietly trains everyone to distrust red CI — worse than not having the tests at all.

What specific problem does consumer-driven contract testing solve that a regular integration test can't?

A regular integration test needs both the consumer and the real, running producer in the same environment at once, which stops scaling past a handful of services. Contract testing lets the consumer publish what it actually expects as a machine-checkable contract, and the producer verifies that contract against its own code in its own CI, in isolation — so the two teams stay compatible without either one running the other's full system.

If you already have contract tests, do you still need end-to-end tests between two services?

Yes, but far fewer of them. Contract tests catch API-shape drift fast and cheaply but don't prove the two services behave correctly together under real network conditions, real data volume, or a real deployed topology. Contract tests answer "did we break the interface"; a small number of true E2E tests answer "does the whole system actually work."

Why use Testcontainers against a real Postgres instead of mocking the repository layer?

A mock only returns what you told it to return, so it can never catch a bug that only exists in the real dependency's actual behavior — a unique constraint violation, a deadlock from lock ordering, or exact serialization quirks. Testcontainers runs the real engine in a disposable container, so the test exercises the same SQL, constraints, and failure modes production will actually hit, while staying fully isolated per run.

Doesn't spinning up a real database in every test suite make CI painfully slow?

It's slower than an in-memory fake, but the fix isn't avoiding real dependencies — it's keeping the number of tests that need one deliberately small by pushing pure logic into unit tests that need nothing at all. A handful of Testcontainers-backed tests per module, run in parallel across CI shards, costs far less than a fast suite that routinely misses bugs a real dependency would have caught.

What is mutation testing, described the way an interviewer wants to hear it?

A mutation testing tool rewrites your production code with small deliberate bugs — flipping a comparison, changing a return value, removing a line — reruns your test suite against each mutant, and reports the percentage your tests actually caught. It measures whether your tests would notice a real bug, not just whether they executed the code.

Give a concrete example of a 100%-covered test that mutation testing would expose as weak.

A test that calls transferFunds(payer, payee, amount) and only asserts assertDoesNotThrow(...), with no assertion on the resulting balances, executes every line and branch, so coverage tools mark it green. A mutant that flips the debit from subtract to add would still pass that test, because nothing checks the outcome — mutation testing catches this because the mutant survives.

Why not just chase 100% line coverage instead of running mutation testing?

Line coverage proves a line executed during some test, not that any test would fail if that line's logic were wrong — it measures what ran, not what was verified. Teams that optimize for the coverage number tend to accumulate tests that call code without asserting on its behavior, which passes the metric while leaving the suite's actual bug-catching ability unmeasured and often quite low.

Do you run mutation testing on every commit the way you run unit tests?

No — it's expensive, because it reruns the relevant test suite once per generated mutant, so most teams run it on a schedule (nightly or weekly) or scoped to a module under active review. It's a periodic health check on test quality, not a per-commit CI gate the way the test suite itself is.

What's the difference between chaos testing for correctness and SRE-style chaos engineering?

SRE-style chaos engineering (killing a pod, saturating a network link in a live or staging environment) validates operational resilience — does the system stay up and recover. Chaos testing for correctness is narrower and runs inside an automated integration test: you make a dependency time out or return a partial response, then assert your specific fallback or retry code path actually executes and produces the right result, not just that the system survives.

Why does a resilience code path need a dedicated test instead of trusting it because the happy path is well tested?

A fallback or retry branch is often the least-executed code in the service — in production it only runs during the rare window when a dependency is actually degraded. If nothing forces that branch to run in CI, it can silently rot (a typo, a swallowed exception, a stale error format assumption) for months, right up until the one incident where it was actually needed.

What's a concrete way to inject a timeout or partial-response failure into an integration test?

Pause or kill a Testcontainers container mid-test, put a Toxiproxy container in front of the real dependency to inject latency or connection resets on demand, or point the client at a WireMock stub configured to return a slow or truncated response for one specific call — any of these forces the exact failure condition the resilience code was written to handle.

What's the actual difference between quarantining a flaky test and just deleting or ignoring it?

Quarantining moves the test out of the blocking CI gate so it stops failing unrelated builds, but keeps it running and tracked, usually with an owner and a deadline. Deleting or silently ignoring it removes the coverage entirely with no record and no pressure to fix it — whatever bug that test used to catch is now unguarded and nobody is tracking that fact.

What are the most common root causes of flaky tests, and how do you tell them apart?

Test-ordering dependencies usually show up as failures that disappear when the test runs alone; shared mutable state looks similar but only reproduces under parallel execution; real timing and concurrency races reproduce more under load or on a slower runner; and external network calls fail in a way correlated with the third party's own incidents, not your code changes. Reproducing under different conditions is usually enough to sort a flake into one of these buckets.

How do you decide how many CI shards to run a test suite across?

It's a direct trade-off between feedback latency and compute cost — doubling shards roughly halves wall-clock time until per-shard fixed costs (container startup, dependency provisioning) start to dominate, after which more shards buy diminishing returns for real money. Most teams pick the smallest shard count that gets pull-request feedback under a target, commonly 5–10 minutes, rather than maximizing parallelism for its own sake.

Can test sharding itself introduce or reveal flakiness?

Yes, if tests were relying on run order or on state left behind by tests that used to run in the same process, splitting the suite across shards can separate a test from the setup it was silently depending on — which is often how a pre-existing ordering bug first gets noticed. Sharding is, in effect, a stress test for hidden test interdependencies.

Where should contract tests run relative to a service's own unit and integration tests in CI?

On the consumer side, publishing the contract is a fast step alongside unit tests, since it only needs the consumer's own code. On the producer side, contract verification runs after unit tests but before deployment, because a broken contract should block a release the same way a failing unit test does — it's evidence the producer is about to ship an incompatible change.

Why might a team pick Spring Cloud Contract over Pact, or vice versa?

Spring Cloud Contract is producer-driven — the producer defines contracts and generates both a verification test and a WireMock stub for consumers, which fits teams where the producer naturally owns the API shape and consumers are mostly internal. Pact is consumer-driven — each consumer records what it actually calls, and a Pact Broker aggregates and verifies across every consumer — which fits better with many independent consumer teams where the contract should reflect real usage.

If a test suite takes 45 minutes and fails one build in five for no code-related reason, what's the first thing you'd actually do?

Separate the two problems, since flakiness and slowness usually have different causes. Quarantine the currently flaky tests immediately so the team regains a trustworthy signal, then look at the pyramid shape — a 45-minute suite is almost always evidence of too many slow integration or E2E tests doing work cheaper unit tests could cover, and shrinking that top layer helps both speed and reliability at once.

How would you explain to a skeptical engineer why a slow, flaky end-to-end suite is worse than a smaller, faster one?

A test suite's value comes from being run and trusted; a suite so slow and unreliable that engineers routinely skip it or re-run it until green has effectively stopped functioning as a safety net, regardless of how much code it technically covers. A smaller, fast, reliable suite that engineers actually run and believe catches more real regressions in practice than a large one nobody trusts.

If an interviewer asks for the single most important idea behind this whole strategy, what do you say?

Match the cost of a test to what it actually needs to prove — push verification as far down the pyramid as it can go, use real dependencies only where a real dependency's behavior is the thing under test, measure whether tests would catch a bug rather than whether they ran, and treat a flaky or slow suite as a bug in the testing strategy itself, not a fact of life to work around.

Related guides

Update these hrefs to your published Blogger post URLs once each page is live.

No comments
Leave a Comment