In a Laravel 13 app, how do tests in tests/Feature differ from tests in tests/Unit, and which base class does each extend?
answer
- which one boots the application
- Tests\TestCase vs PHPUnit\Framework\TestCase
- createApplication requires bootstrap/app.php
- A facade root has not been set
- #[UnitTest] skips booting, since 13.3
basics
~10 sFeature tests extend Tests\TestCase, which boots the whole Laravel application before every test, so HTTP calls, the database, facades and the container work. The skeleton's unit tests extend PHPUnit\Framework\TestCase directly and boot nothing.
solid answer
~40 sThe split is about **what gets booted**, not what gets tested. `tests/Feature` classes extend `Tests\TestCase`, an empty class that extends `Illuminate\Foundation\Testing\TestCase`; its `setUp()` requires `bootstrap/app.php` and bootstraps a fresh application for every test method, so `$this->get()`, facades, `config()` and database traits all work. `tests/Unit` classes generated by `make:test --unit` extend `PHPUnit\Framework\TestCase`, so there is no container: calling a facade throws `RuntimeException: A facade root has not been set.` Unit tests are fast and suit pure classes; feature tests give more confidence, and the docs say most of your tests should be feature tests. The base class, not the folder, decides booting, and since Laravel 13.3 a `#[UnitTest]` attribute lets one method of a `Tests\TestCase` class skip booting.
code
php · 16 lines<?php
namespace Tests\Unit;
use App\Billing\BoxPriceCalculator;
use PHPUnit\Framework\TestCase;
class BoxPriceCalculatorTest extends TestCase
{
public function test_annual_plan_gets_two_free_boxes(): void
{
$calculator = new BoxPriceCalculator(monthlyPrice: 30);
$this->assertSame(300, $calculator->annualPrice());
}
}go deeper
Recall the two folders, the two base classes, and that make:test --unit writes the plain PHPUnit one. Name the facade-root error as the symptom of using Laravel inside a unit test.
Explain that Illuminate\Foundation\Testing\TestCase requires bootstrap/app.php and bootstraps a fresh app in setUp for every method, and that the base class, not the folder, decides this.
Treat the base class as a speed lever: move pure logic off Tests\TestCase, use #[UnitTest] for stray methods, and defend a mostly-feature suite with its confidence argument.
Frame the Feature/Unit mix as a team convention: what confidence each layer buys, what boot cost it charges, and how to keep the rule simple enough that reviewers can enforce it.
## Two directories, two base classes A new Laravel 13 application ships a `tests` directory with two folders and one shared base class: | Location | Generated by | Extends | Boots Laravel? | |---|---|---|---| | `tests/Feature/*Test.php` | `php artisan make:test RenewalTest` | `Tests\TestCase` | yes, for every test method | | `tests/Unit/*Test.php` | `php artisan make:test PriceTest --unit` | `PHPUnit\Framework\TestCase` | no | | `tests/TestCase.php` | the skeleton | `Illuminate\Foundation\Testing\TestCase` | the base that does the booting | In the slim skeleton `Tests\TestCase` has an **empty body**. Older applications carried a `CreatesApplication` trait in the `tests` folder; current Laravel moved `createApplication()` into the framework's own `Illuminate\Foundation\Testing\TestCase`, which requires `bootstrap/app.php` and runs the console kernel's bootstrappers. That empty class still matters: it is the place to put helpers or traits that every feature test in your app should share. ## What "booting" buys a feature test The framework base class mixes in a set of **`Interacts*` concerns** and `MakesHttpRequests`. Because a fresh application instance is created in `setUp()` and torn down in `tearDown()`, a feature test can: - send in-process HTTP requests with `$this->get('/boxes')` or `$this->postJson(...)` and assert on the response; - use facades, helpers such as `config()` and `route()`, and the service container; - use the database reset traits and model factories; - swap real services for fakes and mocks. The cost is time: loading every service provider and every config file for each test method. A suite of a few thousand feature tests spends a noticeable share of its run in that bootstrap. ## What a unit test gives up A class extending `PHPUnit\Framework\TestCase` is plain PHPUnit. Nothing registers a container or sets the facade application, so: 1. `Cache::get('plan')` or any other facade call throws `RuntimeException` with the message **"A facade root has not been set."** 2. Helpers such as `config('app.name')` try to resolve a service from a container that holds nothing, and fail. 3. There is no database connection, no HTTP kernel and no `$this->get()`. In return the test runs in microseconds. That makes the Unit folder the right home for **pure logic**: a price calculator for a subscription box, a date rule deciding the next shipping cycle, a value object. The Laravel docs are explicit that **most of your tests should be feature tests**, because they exercise the system as a whole. ## The folder does not decide; the base class does A frequent misunderstanding is that putting a file in `tests/Unit` makes it a unit test. The folder only groups files for the runner. A class in `tests/Unit` that extends `Tests\TestCase` boots the full application; a class in `tests/Feature` that extends `PHPUnit\Framework\TestCase` does not. What `make:test` does is pick both together: without `--unit` it writes a `Tests\Feature` class extending `Tests\TestCase`, with `--unit` a `Tests\Unit` class extending `PHPUnit\Framework\TestCase`. ## Skipping the boot for one method Laravel 13.3 added the **`#[UnitTest]`** attribute (`Illuminate\Foundation\Testing\Attributes\UnitTest`). Put it on one method of a `Tests\TestCase` subclass and the framework base class's `setUp()` and `tearDown()` return early for that method, so it runs without booting the application. It fits a feature test class that is mostly HTTP tests but has one method checking a pure helper; the method then has the same limits as a unit test, facades included. ## A quick decision rule When writing a new test for the subscription-box app, ask one question: does the code under test need anything the application provides? - It needs routes, the database, config, queues or any facade: write a **feature** test on `Tests\TestCase`. - It needs only its constructor arguments and plain PHP: write a **unit** test on `PHPUnit\Framework\TestCase`. - It sits in a feature class but is pure: keep it there and mark it `#[UnitTest]`. The rule keeps the fast tests fast without forcing anyone to fake the framework by hand. ## How interviewers use this question At a junior screen the expected answer is the table above plus the facade error. The follow-ups usually probe: - whether the candidate knows which base class `make:test --unit` produces; - why a "unit" test that calls `User::factory()` fails; - when it is worth paying the boot cost, and why a mostly-feature suite is the documented recommendation rather than a smell. A strong candidate also says that the base class choice is a speed lever: moving pure-logic tests off `Tests\TestCase` is one of the cheapest ways to shorten a slow suite, because it removes a full application bootstrap per method.
- Why does a test in tests/Unit that calls User::factory()->create() fail?If the class extends `PHPUnit\Framework\TestCase`, no application is booted, so there is no database connection, no container and no facade root. Eloquent cannot resolve a connection and the call errors. Either extend `Tests\TestCase` and use a database reset trait, or keep the test pure and pass plain objects instead of persisted models.
- What would you put inside the skeleton's empty Tests\TestCase class?Anything every feature test should share: helper methods such as a `signInAsSubscriber()` wrapper, traits every test should use, or a `setUp()` override that calls `parent::setUp()` first and then configures common defaults. Keeping it in one class avoids copying setup across dozens of files.
- When would you use #[UnitTest] rather than moving the method to tests/Unit?When the method sits naturally beside feature tests of the same class, for example a pure helper check inside `BoxCatalogTest`. The attribute skips booting for that method only, which saves the bootstrap cost without splitting related tests across folders. It needs Laravel 13.3 or later.
A feature test is a dress rehearsal with the whole theatre lit, crew on duty and props on stage; a unit test is an actor running lines alone in a quiet room. The room is faster, but nothing that needs the crew is available there.
saying these in an interview costs you the question
- A test in tests/Unit never boots Laravel, whatever class it extends.
- Unit tests can use facades because Laravel boots lazily on first use.
- Tests\TestCase boots the application once per class, not per test method.
- Most Laravel tests should be unit tests; feature tests are a smell.
- make:test without flags writes a PHPUnit\Framework\TestCase class.