You inherited a codebase with no trustworthy tests, and the next release is already scheduled. The popular response is to hire experienced developers, add end-to-end coverage, and let delivery resume. I would reverse that order. Until you can identify which existing behaviors matter and reproduce them cheaply, more developers will produce more changes than you can confidently verify.
A larger test-writing team will amplify uncertainty before it reduces it
Top Developers in Eastern Europe for Software Projects can help shortlist candidates, but a hiring list cannot tell a new developer which undocumented behavior your customers rely on. That distinction matters most when the codebase has no tests: a clean-looking refactor can change an invoice total, authorization decision, or retry path without breaking the build. Seniority helps someone find likely risks; it does not supply missing evidence about production behavior.
The tempting assignment is “add comprehensive tests while learning the system.” I would not commission a broad test suite as the first deliverable, because unfamiliar developers must otherwise decide which accidental behaviors to preserve and which to correct. A large suite built on those guesses can become an obstacle to necessary fixes. Start with a narrower assignment: choose one pending change, identify its observable boundary, and capture evidence for the behavior that change might disturb.
Ask the developers and the person who requested the change to walk through a real request together. Record the input, the relevant database state, the output, and any side effect another system can see. For an order endpoint, that might mean the HTTP status, total in minor currency units, persisted status, and whether a message was published. Do not require agreement about the internal design yet. An inherited implementation may be awkward, but externally visible behavior gives QA a testable question: “Did this change alter what the caller receives or what the system records?”
Put a short limit on this reconnaissance. Treat 2 working days as an adjustable investigation budget for the first change, not a promise that every dependency will be understood. At the end, require a request that another engineer can run, a list of assertions with their owners, and a list of unknowns. If nobody can say whether an observed result is correct, label it “current behavior” rather than silently promoting it to a requirement. That label lets a later product decision change the behavior without being framed as a test regression.
Hiring can still proceed, but change the interview exercise. Give candidates a small unfamiliar code path and ask them to distinguish a useful characterization test from an assertion that merely mirrors the implementation. A candidate who asks who owns the expected result is more useful here than one who produces fifty assertions quickly, because your immediate constraint is reliable judgment, not typing speed.
End-to-end coverage is the wrong first default when the test environment lies
A browser test feels reassuring because it resembles a user journey. In an untested codebase, it often crosses the least controlled boundaries first: shared accounts, mutable records, asynchronous workers, and third-party services. A failure then tells you that something changed, but not whether the application, fixture, network, or test caused it. That ambiguity is expensive for a QA engineer who must decide whether to block a release.
Start at the narrowest observable boundary that can catch the pending mistake. For a pricing change, an HTTP request against a seeded order may be more valuable than a Playwright browser flow, because it checks the returned total without relying on login UI and page timing. For a broken checkout interaction, Playwright may win because an API test cannot detect an unusable button. Neither choice is a rule about test pyramids; the winning test is the one whose failure points most directly to the risk being changed.
Here is a small smoke check for a read-only order route. It runs with Bash, curl 7.76.0 or later, and jq 1.6 or later; set the three environment variables to a seeded fixture before running it. The endpoint and response fields are examples to replace with your application’s real contract.
#!/usr/bin/env bash
set -euo pipefail
: "${BASE_URL:?Set BASE_URL}"
: "${TOKEN:?Set TOKEN}"
: "${ORDER_ID:?Set seeded numeric ORDER_ID}"
curl --fail-with-body --silent --show-error \
--connect-timeout 3 --max-time 10 \
-H "Authorization: Bearer ${TOKEN}" \
-H 'Accept: application/json' \
"${BASE_URL%/}/api/orders/${ORDER_ID}" |
jq -e --argjson id "$ORDER_ID" \
'.id == $id and (.total_cents | type == "number")'
The 3-second connection timeout and 10-second overall timeout are starting values to tune against your staging network, because a timeout that is too tight creates noise while one that is too loose delays feedback. This check proves little about arithmetic: it only proves that the known order can be read and has a numeric total. Add an expected total only after someone authorized to define that result has checked the fixture. Otherwise, a test can faithfully preserve an existing billing error.
Once a request is stable, move it into pytest 8.x so fixtures can create and clean up test data. Keep one ownership rule: either the test creates its order in an isolated PostgreSQL database, or a designated fixture process does; two writers make failures hard to reproduce. Run the narrow check in GitHub Actions on pull requests, but keep the first gate small. GitHub’s published 6-hour limit for a GitHub-hosted runner job is a platform ceiling, not a reasonable feedback target for a developer waiting to merge.
Characterization tests beat blanket coverage until behavior has an owner
There are two plausible first investments: a characterization suite around a few risky boundaries, or a comprehensive end-to-end suite across user journeys. Characterization wins when expected behavior is disputed or test data is unstable, because each case can carry its source of truth: an approved example, a support incident, or an explicit product decision. It costs time to investigate each assertion and leaves many paths untested. Comprehensive end-to-end coverage wins when journeys and fixtures are already stable and the main risk is a cross-system interaction; it costs more environment maintenance and makes a failing run slower to diagnose.
For your inherited system, create a behavior ledger beside the tests. Each entry should name the route or event, the fixture, the expected outcome, who approved it, and the change that made it worth protecting. Link the test to the ledger entry in its name or comment. This is less glamorous than a coverage percentage, but it lets the next QA engineer determine why an assertion exists before deleting or “fixing” it.
Use different tools for different uncertainty. pytest can pin a deterministic calculation. Pact can express a consumer-driven contract when a separate client depends on a JSON field, provided the consumer actually verifies the contract. WireMock can replace an unreliable upstream HTTP dependency when its response shape is known. Playwright should cover the few interactions whose browser behavior matters. OpenTelemetry traces can show which service handled a request, while Prometheus and Grafana can help establish whether a test environment is experiencing errors during a run. None of those observations alone establishes the expected business result; they answer different questions.
Coverage reports from coverage.py are useful for locating untouched code, but I would not set a project-wide percentage target first, because a high number can reward assertions that prove only that a method ran. Set a provisional goal of 5 approved examples around the next release’s highest-risk behavior, and revise that target after seeing whether the examples detect meaningful changes. For each case, deliberately alter a copy of the code or fixture and confirm that the test fails for the intended reason. A test that remains green when its protected behavior changes is documentation at best.
Record flaky runs rather than hiding them with automatic retries. If a test fails once and passes on rerun, preserve the first failure’s logs, fixture ID, and trace ID; the rerun is evidence of nondeterminism, not proof of safety. Measure the local retry rate over a defined period before setting a tolerance, because an unexplained “acceptable” flake rate gives the team permission to ignore a real regression.
External developers need bounded evidence, not a mandate to move faster
Once the first checks exist, an Eastern Europe team can contribute without having to guess whether every surprising result is a defect. Give each change request the behavior ledger entry, a reproducible fixture, the test command, and an explicit decision maker for ambiguous output. Require the pull request to state which behavior changed, which behavior was retained, and what remains unverified. That format costs more preparation per ticket, but it reduces the review work caused by unstated assumptions.
Is your Eastern Europe team improving SLOs or adding toil asks an operational question, but it cannot answer whether a green release preserved an undocumented order rule. An SLO based on availability or latency can remain healthy while a calculation is wrong, because the service may return incorrect responses quickly and consistently. Use SLOs to watch service reliability; use approved examples and change-specific checks to judge this release’s behavior. Conflating the two gives QA a green dashboard without a defensible sign-off.
Set a handoff rule before adding more contributors. A developer may change code and tests in the same pull request, but an assertion about disputed behavior needs approval from someone other than the person implementing it. That separation is not distrust of a remote team; it prevents any implementer from defining success solely by what their new code does. If an urgent fix cannot wait for approval, mark the assertion provisional and name the person who will resolve it afterward.
Review the first few pull requests for evidence quality rather than commit volume. Can you run the reported fixture? Does a failing test identify the behavior at risk? Does the change avoid unrelated cleanup that makes comparison harder? Those questions make a useful acceptance gate because they connect QA sign-off to observations you can reproduce, even while the wider codebase remains largely untested.
Your first release decision should rest on one reproducible case
Pick the next risky change and ask for its smallest real input, expected output, and approving owner today. Run that case against the current build, save the result, then run it against the proposed change. If the expectation is disputed, record the dispute before adding a passing assertion. That single comparison will tell you what evidence your team lacks—and what an additional developer should be hired to produce.
