What This Is
Unit testing is the practice of writing small, fast, isolated tests that verify a single function, method, or class in isolation from the rest of the system. A unit test does not touch the database, does not make HTTP calls, and does not depend on other components — it gives one piece of code one input and asserts the output is correct.
Unit tests are the base of the testing pyramid because they are cheap to write, fast to run, and pinpoint exactly where a regression occurred. A unit test suite of a thousand tests can run in seconds, which means developers can run it before every commit without slowing down. That speed is what makes unit testing valuable — it changes the feedback loop from “I will find out if this broke when QA tests it next week” to “I know it broke before I finish typing.”
The work is not simply about writing tests — it is about writing tests that catch real bugs without creating maintenance burden. A unit test that just mirrors the implementation will pass when the code is wrong and fail when the code is refactored. A good unit test asserts behaviour, not structure.
When You Need This
Unit testing is the right starting point when:
- Your codebase contains business logic — calculations, validation rules, state transitions, formatting — that has clear inputs and outputs
- Developers are afraid to change existing code because there is no fast way to know if they broke something
- You want a fast feedback loop during development — tests that run in seconds, not minutes
- You are refactoring or modernising legacy code and need a safety net to prove behaviour is preserved
- Your team is adding new features to a system that has no tests at all — unit tests are the cheapest way to start protecting new code without retrofitting tests for everything that already exists
This is not the right service as a standalone solution if your bugs are mostly happening at integration boundaries — between your code and the database, your code and a third-party API, or your frontend and your backend. Unit tests do not catch those, by design. We will recommend Integration Testing or End-to-End Testing where they fit better.
How We Work
Unit testing engagements focus on what is worth testing before they focus on writing tests. Not every line of code needs a unit test — a getter that returns a stored property is not adding signal. The high-value targets are functions with branching logic, calculations, validation, transformations, and anything that has historically had bugs.
We write tests around the public interface, not the implementation. Tests that assert “this method calls that other method” are coupled to the implementation and break on every refactor. Tests that assert “given this input, this output is produced” survive refactoring and document behaviour. We default to the latter.
We name tests so failures are useful. A test failure with a message like “Assertion failed at line 47” is not actionable. We name tests after the behaviour they verify (it_rejects_an_invoice_with_negative_total), so a CI failure tells you what is broken before you even open the file.
We use test doubles sparingly. Mocks, stubs, and fakes are useful for isolating code that genuinely depends on external systems, but a test suite that is mostly mocks is testing the mocks, not the code. We mock at boundaries — HTTP clients, third-party services — and use real objects everywhere else.
What You Get
- Unit test suite covering the high-value logic in your codebase, structured by feature or domain
- Behaviour-focused test names that read as documentation of what the system does
- Coverage analysis showing which branches and edge cases are protected
- Test doubles strategy for the boundaries that need them, applied consistently
- CI integration so the suite runs on every push and blocks merges on failure
- Documentation on how to add new unit tests and how to debug failures
Technologies We Use
- PHPUnit with Laravel’s testing helpers for PHP backends — factories, assertions, and database transactions
- Pest where the team prefers its expressive syntax
- Vitest for modern Vite-based JavaScript and TypeScript projects
- Jest for established Node and React projects
- Mockery and Prophecy for PHP test doubles where needed
- Code coverage tools (Xdebug, PCOV, Istanbul) for measuring branch coverage
The choice of framework follows the project’s stack — not the other way around.
Related Systems
Unit testing protects the internal logic of every system we build. A pricing system with VAT, discounts, and tiered pricing has dozens of edge cases that only unit tests can exhaustively cover. A workflow engine with state transitions needs unit tests for each transition rule.
Talk to Us About Adding Unit Tests
If your codebase is growing faster than your confidence in it, get in touch and we will scope a unit testing engagement starting with the highest-risk areas.