The #[UnitTest] Attribute (Laravel 13)

+15 Mana ✨

Introduction

Before Laravel 13 you had a binary choice: extend Tests\TestCase (app booted, slow, every helper available) or PHPUnit\Framework\TestCase (no app, fast, no helpers). Mixing styles forced you to split related tests across two files. Laravel 13 added the #[UnitTest] attribute so you can flag individual test methods as "skip the app boot just for me."

Key Concepts

  • Class-level base: Determines the default mode — booted or not.
  • Method-level attribute: #[UnitTest] on a single method overrides the class default for that method only.

Real World Context

A ProductTest that has 8 feature tests hitting the product controller and 2 pure unit tests on a price-formatting helper. You want all 10 in one file for cohesion, but the 2 helper tests shouldn't pay the cost of booting Laravel.

Deep Dive

Mixing booted and non-booted tests

php
<?php

namespace Tests\Feature;

use App\Services\PriceFormatter;
use Illuminate\Foundation\Testing\Attributes\UnitTest;
use Tests\TestCase;

class ProductTest extends TestCase
{
    // Normal feature test — Laravel is booted
    public function test_admin_can_list_products(): void
    {
        $admin = User::factory()->admin()->create();

        $this->actingAs($admin)
            ->get('/admin/products')
            ->assertOk();
    }

    // Pure unit test — app boot is skipped for this method
    #[UnitTest]
    public function test_price_formatter_rounds_to_two_decimals(): void
    {
        $formatter = new PriceFormatter();

        $this->assertEquals('$9.99', $formatter->format(9.994));
    }
}

When PHPUnit runs test_admin_can_list_products, Laravel's CreatesApplication trait boots the container. When it runs test_price_formatter_rounds_to_two_decimals, the attribute tells the runner to skip that bootstrap — so $this->app, actingAs(), and every other helper that relies on the container are unavailable inside that method.

What you lose inside a #[UnitTest] method

Inside an attribute-marked method you cannot use:

  • $this->get(), $this->post(), and other HTTP helpers.
  • $this->app, $this->actingAs().
  • Eloquent models (they need the connection from the container).
  • Facades (they resolve through the container).

You can still use:

  • Pure PHP code.
  • Classes instantiated directly with new.
  • Mockery directly.
  • Data providers.

When to reach for it

Use #[UnitTest] when a test is genuinely pure and you want to colocate it with related feature tests for readability. If a whole file is pure, just extend PHPUnit\Framework\TestCase — the attribute is for mixed files.

Common Pitfalls

  1. Using Eloquent inside a #[UnitTest] method — models need the DB connection resolved by the container. The test throws a confusing "no connection" error.
  2. Applying the attribute to setUp/tearDown — it only applies to test methods. Setup still runs under the class's base mode.

Best Practices

  1. Keep #[UnitTest] methods pure — if you find yourself wanting a facade, delete the attribute and let the app boot.
  2. Group #[UnitTest] methods at the bottom of the class — visual separation makes it obvious which section has which capabilities.

Summary

  • #[UnitTest] is a Laravel 13 attribute that skips app boot for a single test method.
  • Lives in Illuminate\Foundation\Testing\Attributes.
  • Use it to colocate pure-logic tests with their feature-test neighbors without slowing the whole suite.
✓ Completed