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.
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
| Layer | What it proves | Where it runs | Who owns it |
|---|---|---|---|
| Unit tests | A business rule works as written | Locally and on every commit | The engineer who wrote it |
| Integration tests | The code works with its dependencies | On every commit, against real services | The feature team |
| Contract tests | Two services still fit together | In both teams’ pipelines | Both teams, jointly |
| End-to-end tests | A complete user journey still works | On merge, against a deployed build | The feature team |
| Performance tests | It handles the expected load | Before each release, and nightly | Engineering with the service owner |
| Exploratory testing | What a script wouldn’t think to check | On a build, before sign-off | A tester using their judgment |
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.
- 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.
- 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.
- 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.
- 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.
- 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.
