Back to blog

Software testing

What Is Unit Testing? Fast Feedback for Safer Software Changes

Learn what unit testing checks, how it gives developers fast feedback and why broader tests are still needed for reliable software.

Unit testing is one of those software terms that sounds more complicated than the idea behind it. A developer gives one small part of an application a known input, checks the result and records the expectation so it can be checked again whenever the code changes. That fast feedback is valuable, but it only tells the truth when broader tests cover the parts a small isolated check cannot see.

01

A small pricing change should not need a nervous afternoon

Imagine a customer portal that calculates renewal prices. The rule begins simply: take the annual price, apply an approved percentage discount and never allow the final amount to fall below the minimum fee.

Six months later, the application supports several products, special renewal terms and different minimum fees. A developer is asked to change one rule. The arithmetic is still simple, but the consequences are not. A small mistake could put the wrong figure on every renewal generated that afternoon.

Without automated checks, confidence often comes from reading the code, trying a few examples and hoping the less obvious cases have not changed. A unit test turns those examples into repeatable evidence. It can check the normal discount, a zero discount, the maximum permitted discount and the minimum fee boundary in seconds.

This is the practical value of unit testing. It shortens the distance between making a change and discovering that a rule no longer behaves as expected.

02

What unit testing actually means

A unit test checks a small, logically isolated piece of code. Depending on the language and design, that unit may be a function, a method, a class or a small group of closely related objects. The exact boundary matters less than the behaviour of the test.

A useful unit test is fast, repeatable and deterministic. The same input should produce the same result. It should not depend on a live database, network connection, file system, current clock or third party service unless those dependencies have been replaced by controlled test versions.

Most unit tests follow a simple shape. Arrange the input and any controlled dependencies. Act by calling the behaviour under test. Assert that the result or visible outcome is correct.

For the renewal calculator, the test might arrange a price of £1,000 and a discount of 10 per cent, call the calculator and assert that the result is £900. Another test can prove that a discount never takes the total below the product's minimum fee.

The test is small, but the language is useful. It describes a business rule in a form the computer can check every time the application is built.

03

Test behaviour, not private wiring

The strongest unit tests focus on what the code promises to do. They give the public behaviour an input and check its result. They do not try to prove every private method was called in one exact order.

That distinction matters because implementation details change during refactoring. A developer may split one method into three, rename an internal helper or replace a loop with a clearer collection operation. If the public behaviour stays correct, a useful unit test should usually keep passing.

A brittle test is tightly coupled to how the answer is produced. It can fail after a harmless internal change, even though customers would see exactly the same result. The team then spends time repairing tests that are acting like surveillance cameras inside the code rather than checks on its promises.

There are exceptions. Sometimes an interaction is the behaviour. If a service must queue a notification once, record an audit event or avoid charging a card twice, the call itself may deserve an assertion. The test should still protect a meaningful outcome rather than an arbitrary implementation detail.

04

Isolate the expensive and unpredictable boundaries

Real application code rarely lives alone. A renewal service may ask a repository for the current policy, call a pricing provider, read the current date and publish an event when the price changes.

Unit tests can replace these collaborators with test doubles. A stub returns a controlled answer. A mock can also check that an expected interaction took place. PHPUnit provides both mechanisms, and its current guidance recommends favouring interfaces when creating doubles.

Test doubles are most useful at genuine boundaries: databases, remote APIs, queues, payment services, email delivery, file storage and time. Replacing them keeps the test quick and lets it exercise awkward cases without waiting for a real supplier to fail on demand.

Overuse causes a different problem. If every ordinary object is mocked, the test may only prove that the mocks were configured consistently. The application can still fail when the real objects meet. A simple value object or calculation usually provides more confidence when used directly.

A sensible rule is to keep normal in-memory collaborators real when they are quick and predictable, then substitute the boundaries that are slow, external or difficult to control.

05

Unit tests are one layer, not the whole safety net

Different tests answer different questions. Calling all of them unit tests makes the suite harder to reason about and encourages teams to expect the wrong kind of confidence from one layer.

Four testing layers and the questions they answer
Test typeBest questionTypical boundaryWhat it can miss
Unit testDoes this small rule behave correctly for known inputs?One function, class or small group of objectsDatabase mappings, framework configuration and real integrations
Integration testDo these components work together?Code with a database, queue, file store or external service boundaryThe complete user journey and browser behaviour
Feature or API testDoes this application workflow produce the right response?Several application layers, often through HTTPFront end rendering and the real production environment
End to end testCan a user complete the important journey?Browser, application, data and connected servicesMany detailed edge cases because these tests are slower and more expensive

The useful pattern is breadth through small tests and carefully chosen confidence through larger tests. A pricing calculator may have many unit tests around its rules, a smaller number of feature tests proving a renewal request reaches the calculator correctly, and one end to end check proving a member of staff can issue a renewal through the portal.

This layered approach also makes failures easier to understand. When a unit test fails, the problem is usually close to the named rule. When an end to end test fails, the fault might sit anywhere across the browser, network, application, database or supplier.

06

What good unit tests look like in a Laravel application

Laravel includes support for Pest and PHPUnit. Its default test structure separates Unit and Feature tests. Tests in the Unit directory do not boot the Laravel application, so they cannot use the database or framework services. That is useful for plain PHP classes that hold calculations, decisions and other business rules.

Laravel's own documentation also makes an important point: most tests in a Laravel application may sensibly be feature tests because they give stronger confidence that several parts of the system work together. That is not an argument against unit testing. It is a reminder to put each check at the level where it tells the truth.

A discount calculation, date range rule, status transition or permission decision can often be tested as a small PHP unit. A controller route, validation rule, database transaction or JSON response normally belongs in a Laravel feature test. A queue or payment integration may need a combination of feature tests, fakes and a smaller number of checks against the real provider's test environment.

The framework should not be booted merely because it is available. If a class only needs two values to make a decision, bringing in the container, database and application lifecycle makes the test slower and hides the simplicity of the rule.

07

Why teams lose faith in unit tests

Unit tests become unpopular when they create noise instead of information. That usually happens for a handful of recognisable reasons.

Tests share state and fail depending on the order in which they run. They depend on the current time or random data without controlling it. They contain so much setup that the business rule is hard to find. They mock every method call and break whenever code is tidied. They assert that something happened without checking a useful result. Or they take so long that developers stop running them before making a change.

Coverage targets can make this worse. A percentage can show which lines were executed, but it cannot tell whether the important decisions were checked well. A test that calls a method and asserts nothing meaningful can improve coverage while adding no confidence at all.

Good unit tests tend to be boring. Their names describe behaviour. Their inputs are obvious. Their failures point towards one broken expectation. They can run locally and in continuous integration without special sequencing or a helpful network connection.

When a test is awkward to write, it may also be useful design feedback. A class that needs eleven collaborators and several pages of setup probably has more than one responsibility. The answer is not always another mock. Sometimes the code needs a clearer boundary.

08

What unit tests cannot prove

Unit tests do not prove that the application is bug free. They prove only the behaviours that somebody thought to express and the paths the suite actually runs.

They cannot confirm that a database column exists in production, a route uses the correct middleware, an email template renders properly, a queue worker is running or a browser can submit a form. They do not check accessibility, confusing wording or whether the product solves the customer's problem.

They can also preserve the wrong rule perfectly. If the expected renewal price is misunderstood, the test will make that misunderstanding repeatable. Business rules still need review by the people who own them.

This is why unit testing belongs inside a wider delivery practice. Code review, static analysis, integration testing, feature testing, end to end checks, monitoring and human judgement each see a different part of the risk.

09

Start with the rules that would make somebody pick up the phone

Adding unit tests to an established application does not require a campaign to cover every class. Begin where a wrong answer would be expensive, embarrassing or difficult to spot.

Pricing, tax, eligibility, permissions, workflow transitions, date calculations, capacity limits and document validation are good candidates because they contain decisions with clear inputs and outcomes. Add tests when repairing a defect so that the exact failure cannot quietly return. Add them before refactoring a risky rule so the current behaviour is visible before its internal structure changes.

Return to the renewal calculator. The value of its tests is not the number of assertions or a perfect coverage report. It is that a developer can change one rule, run the suite and learn quickly whether the ordinary cases and awkward boundaries still behave as the business expects.

Unit testing gives software teams fast feedback close to the code. Used alongside broader tests, it makes small changes less mysterious and important business rules easier to own.

DanJMills builds, modernises and supports Laravel and Vue applications for established businesses. If an important application is difficult to change because nobody knows what a small edit might break, the first useful step is to identify the critical rules and build a proportionate test safety net around them.

Useful questions

Useful unit test checklist:

  • Does the test describe one observable behaviour in plain language?
  • Can it run without a live database, network, file system or third party service?
  • Will the same input produce the same result every time?
  • Does it check a useful outcome rather than private method calls?
  • Are real, simple collaborators used where mocks would add noise?
  • Are external boundaries replaced with controlled test doubles?
  • Does a failure point towards one broken business rule?
  • Can the test run quickly on a developer machine and in continuous integration?
  • Is a feature or integration test covering the parts this unit test cannot see?
  • Would the test still be valuable if the internal implementation changed?
Explore Laravel and Vue development
Daniel Mills

Written by Daniel Mills

Business understanding and hands-on software delivery.

I help owners and teams improve the software they rely on, replace fragile processes and turn new ideas into practical systems people can actually use.