.pageTitle {

So... What Kind of Test Is This?

.published {

June 2025

} }

I've been writing software for a long time. I've also been writing tests for quite a long time.

And yet, every now and again, someone will ask: “Is this a unit test or a feature test?” And I'll stop, look at the test, look at the code, look back at the person who asked, and think: does it actually matter?

Well, yes. But also... not nearly as much as we sometimes make it sound.

We've accumulated a whole vocabulary for testing software: unit tests, feature tests, integration tests, acceptance tests, end-to-end tests, smoke tests, regression tests, contract tests, functional tests, and probably several others I've forgotten while writing this.

First: what are we actually testing?

The easiest way I've found to understand the different types of tests is to stop thinking about the names for a moment. Instead, ask: “What am I trying to prove?”

  • Does this little bit of code calculate the correct value?
  • Does this Laravel service do what I expect?
  • Does this API endpoint return the right response?
  • Does my application actually talk to the database correctly?
  • Does our API still conform to what another application expects?
  • Can a real user actually complete the process?
  • Is the application basically alive after deployment?

Those are all tests. They're just testing different things.

Unit tests

A unit test tests a small, isolated piece of code: usually a class, method or function.

class PriceCalculator
{
    public function addVat(float $price): float
    {
        return $price * 1.2;
    }
}
public function test_it_adds_vat()
{
    $calculator = new PriceCalculator();

    $this->assertEquals(120, $calculator->addVat(100));
}

There's no database, HTTP request, browser, authentication, queue or external API. Just input → code → output.

The big advantage is speed. You can have thousands of these tests and run them very quickly. Because they're isolated, when one fails you generally have a pretty good idea where the problem is.

Feature tests

Now suppose our application has POST /orders. A user submits an order; the application validates it, stores it, calculates the total and perhaps sends an email. A feature test might exercise that whole process.

$this->post('/orders', [
    'product_id' => 123,
    'quantity' => 2,
])->assertSuccessful();

We might then check that an order exists in the database. This isn't testing one individual method: it's testing a feature of the application.

A unit test might prove, “the order total is calculated correctly.” A feature test might prove, “a customer can place an order.” Both are useful; they're looking at the problem from different distances.

Integration tests

Integration tests take us another step outwards: they test whether two or more components work together. The application and database; the application and a payment API; or the application, database, queue and external API.

A unit test might mock the database. An integration test deliberately doesn't, because we want to know whether our application and the database actually work together. Here's the uncomfortable truth: mocks can lie.

You can have a beautifully written unit test saying everything is fine while the real database is sitting there thinking: “No it isn't.”

End-to-end tests

An end-to-end test behaves like a user: open the application, log in, click something, fill in a form, submit it and check the result. Perhaps using Playwright, Cypress or Selenium.

Open website
     ↓
  Log in
     ↓
Create order
     ↓
    Pay
     ↓
Receive confirmation

They're valuable, but usually slower, more complicated, more fragile and harder to diagnose. If one says the order process failed, you may have to investigate the browser, JavaScript, HTTP, authentication, database, queue and payment service. So we don't necessarily want everything to be an end-to-end test.

Smoke tests

A smoke test is basically asking: “Is the thing alive?” The name comes from switching something on and checking that it doesn't immediately start smoking.

You don't need to test every feature. You just want to know that the application hasn't fallen over completely.

Does GET /health return 200? Can we load the homepage, connect to the database, or log a user in? If those basic things don't work, there's probably not much point running another 4,000 tests.

Regression tests

Regression is less about what a test looks like and more about why we're running it. A regression test makes sure something that used to work still works.

Fix a bug where customers can't download invoices, and add a test. Six months later someone changes the invoice system and the test fails. Excellent. It has caught a regression. A unit, feature or end-to-end test can all be regression tests.

Contract tests

These become useful when APIs and multiple systems are talking to each other. An external application may expect:

{
    "id": 123,
    "name": "The Grand Hotel",
    "rooms": 42
}

If someone changes rooms to room_count, our API tests might all pass and our application might work perfectly. But the other application has just exploded. A contract test checks the agreement: “Are we still providing what the other system expects?”

Acceptance and functional tests

Acceptance testing asks whether the software meets the requirement. It's less “does this method return 42?” and more “can the customer actually do what the business asked us to build?”

Given a booking exists
And arrival is more than 24 hours away

When the customer cancels the booking

Then the booking should be cancelled
And the customer should receive confirmation

We're talking about behaviour rather than implementation. The test doesn't particularly care whether the application uses Laravel, Symfony, PHP, Python, a database, or a particularly enthusiastic collection of goats. It cares that the requirement is satisfied.

Functional testing is broadly whether the software performs a required function correctly: can I create a customer, generate an invoice or reset my password? Depending on who you ask, these might instead be feature, acceptance or integration tests. The terminology overlaps.

The important bit

Don't imagine these as completely separate boxes. Imagine them as different dimensions.

How much code?       What are we proving?       Why run it?

UNIT                 CODE                         REGRESSION
  ↓                  ↓                            SMOKE
INTEGRATION          FUNCTION                     CONTRACT
  ↓                  ↓
FEATURE              REQUIREMENT
  ↓                  ↓
END-TO-END           USER JOURNEY

A test can be a feature test which is also a regression test; an integration test which checks an API contract; or an end-to-end test which verifies an acceptance criterion.

So which tests should we write?

Not “should this be a unit test or an integration test?” but “What risk am I trying to remove?”

  • Worried that a calculation is wrong? Unit test.
  • Worried that components don't work together? Integration test.
  • Worried that an application feature doesn't work? Feature test.
  • Worried that two APIs disagree? Contract test.
  • Worried that a user can't complete an important journey? End-to-end / acceptance test.
  • Just deployed and want to know whether it is alive? Smoke test.
  • Fixed something and don't want it breaking again? Regression test.

I think developers sometimes get too hung up on the terminology. I've seen discussions about whether a particular test is technically a unit test or an integration test, while the actual test is doing a perfectly good job of telling us that the software works.

That's the bit that matters. Tests are there to give us confidence. They should tell us something useful, catch problems, be reasonably fast and maintainable, and give us some idea what has gone wrong when one fails.

In summary

Some names describe what you're testing. Some describe how much of the system you're testing. Some describe why you're testing it. And some are used differently by different teams. The important thing isn't memorising the vocabulary; it's understanding the question each test is trying to answer.

TestBasically asking...
UnitDoes this small piece of code work?
IntegrationDo these components work together?
FeatureDoes this feature actually work?
AcceptanceDoes it meet the requirement?
End-to-EndCan the user complete the whole journey?
SmokeIs the application basically alive?
RegressionDoes something that used to work still work?
ContractDo two systems still agree on how to communicate?
FunctionalDoes the required function work?

And if you're still confused about the difference between a feature test and an integration test, don't worry. You're not alone. I've been doing this for years. I'm still occasionally tempted to call them all: “Tests that stop production breaking.” Which, to be fair, is probably the most useful definition of all.

Don't start with the label.

Start with the risk you are trying to remove.

Mocks can lie.

The real database occasionally has a very different opinion.

An illustrated guide to unit, feature, integration, smoke, end-to-end, regression, contract, acceptance and functional tests
</