Test Automation

Releases shouldn’t depend on how much someone remembered to click through. We build a test suite that runs on every change, and when something fails, it tells you exactly where to look.

Something that worked last month stops working, and nobody notices until a customer does. So every release gets a week of manual testing beforehand, which catches the obvious issues and misses the one that matters. The real fix is a test suite that knows what the software is supposed to do, runs automatically on every change, and gives useful information when a test fails.

The suite nobody trusts

Rerunning until it passes is not testing

The pipeline fails, someone clicks retry, it passes, and the merge goes through. After a few rounds of this, the suite stops being a warning and becomes an obstacle. Flaky tests are usually a design problem, not bad luck: shared state between tests, real network calls, or assumptions about timing. All of these can be fixed.

  • Isolate the flaky tests first, then fix or delete them
  • Replace shared test data and fixed wait times with data each test creates for itself
  • Move tests to a lower layer when a unit test can prove the same thing
  • A failed build blocks the merge, so nobody gets used to ignoring it

Coverage counts lines, not behavior

Code coverage shows which lines of code ran during testing. It doesn’t show whether anything was actually checked. A suite can run most of a codebase while verifying almost nothing, and a high coverage target often leads to exactly that: tests written to touch code rather than to describe what it should do. The number goes up, but your confidence doesn’t.

That’s why we ask two questions about every test. Does it describe something the software is supposed to do, in terms a business stakeholder would recognize? And when it fails, does it point to where the problem is, or only say that a page didn’t load? Coverage is still worth tracking, mainly to find code with no tests at all. It’s an indicator, not a goal.

What each layer is for

LayerWhat it provesWhere it runsWho owns it
Unit testsA business rule works as writtenLocally and on every commitThe engineer who wrote it
Integration testsThe code works with its dependenciesOn every commit, against real servicesThe feature team
Contract testsTwo services still fit togetherIn both teams’ pipelinesBoth teams, jointly
End-to-end testsA complete user journey still worksOn merge, against a deployed buildThe feature team
Performance testsIt handles the expected loadBefore each release, and nightlyEngineering with the service owner
Exploratory testingWhat a script wouldn’t think to checkOn a build, before sign-offA tester using their judgment
Most test suites have too many end-to-end tests and too few unit tests, which is why they are slow and unreliable.

How the work runs

The usual starting point is a suite that already exists and a pipeline nobody fully trusts. The order matters: there is no point adding tests to a build that people have learned to ignore.

  1. 01

    Sort the suite

    We run the existing tests repeatedly and record which fail, which flake and which check features that no longer exist. You receive a list with a recommendation for each test: keep, fix, move to a lower layer, or delete.

  2. 02

    Fix the foundations

    Test data each test creates for itself, environments that start clean, and stubs where a real external call adds only noise. Each fix arrives as a reviewed pull request that names the cause of flakiness it removes.

  3. 03

    Wire up the pipeline

    The suite is split by layer so each part runs where it is cheapest, and merge rules make a failed build final instead of something to retry. You review the pipeline definition like any other code, in a pull request.

  4. 04

    Fill the gaps

    New tests go first where a failure costs most: money, customer data, the paths users take every day. We use AI to draft tests from existing behavior, and an engineer reviews each one, deleting any that merely restate the code.

  5. 05

    Hand it over

    Before the engagement ends, your engineers write new tests with us in the same framework and conventions. You keep a short guide to how the suite is organized and where each kind of test belongs.

What you are left with

Testing stops being a separate event before each release. Once the work is complete, this is what your release process should look like.

A suite that runs on its own

Every push triggers the tests, and a failed test blocks the merge. Nothing depends on someone remembering to check before a release.

Failures that point to the problem

A failed build shows which behavior broke and at which layer, so engineers can start on the fix right away instead of first trying to reproduce it.

Regression testing without the lost week

The checks that used to keep a team busy clicking through the same screens now run automatically, on every change, not only before a release.

Manual testing where it matters

Testers stop rechecking what a machine can check and focus on what it can’t: confusing flows, unusual data, accessibility, and whether the product is actually good.

Tools we test with

The test runner matters less than what you test with it. If you already have a suite, we work within it rather than starting a second one. When the choice is open, we pick what your team can maintain, not what demos best.

  • Playwright
  • Cypress
  • Selenium
  • Jest
  • JUnit
  • xUnit
  • pytest
  • Pact
  • Postman
  • k6
  • JMeter
  • GitHub Actions
  • Azure Pipelines

The questions we get

Not all of it, and aiming for that is how suites become expensive. Automate anything that will be checked more than a few times, anything that guards money or data, and anything a person finds boring enough to do badly. Leave exploratory work, accessibility review and first looks at a new interface to people, because those depend on judgment rather than repetition.

Not sure which one you need?

Describe the problem in a paragraph and we will tell you which service applies.