0

Testing commands — artisan() in feature tests

Intermediate5 min read·lv-25-006

Concept

PHPUnit in Laravel extends the base PHPUnit\Framework\TestCase with Laravel-specific testing utilities. The Laravel TestCase (Illuminate\Foundation\Testing\TestCase) boots the full application for each test, enabling HTTP testing, database interaction, service container access, and facade faking.

Test types:

  • Feature tests (tests/Feature/): Test complete flows — HTTP requests through the stack, multiple components working together. Use RefreshDatabase.
  • Unit tests (tests/Unit/): Test a single class in isolation. The application is NOT booted. No database access. Fast.

RefreshDatabase: Before each test, wraps the test in a transaction (rolled back after) OR runs migrate:fresh (slower but handles certain issues). Use RefreshDatabase for most feature tests. On SQLite (in-memory), migrate:fresh runs once and transactions roll back.

WithFaker: Add to test classes for access to $this->faker (Faker instance). Useful for generating test data inline without factories.

Assertions (TestCase methods):

  • assertTrue(), assertFalse(), assertEquals(), assertNull().
  • assertDatabaseHas($table, $conditions): Assert a row exists.
  • assertDatabaseMissing($table, $conditions): Assert row doesn't exist.
  • assertDatabaseCount($table, $count).
  • assertSoftDeleted($table, $conditions).

Pest PHP (lv-26-009): A modern alternative to PHPUnit. it() / test() functions instead of class methods. Runs on PHPUnit internally.

Code Example

php
<?php
// Feature test — full stack
namespace Tests\Feature;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;

class UserRegistrationTest extends TestCase
{
    use RefreshDatabase, WithFaker;

    public function test_user_can_register(): void
    {
        $response = $this->post('/register', [
            'name'                  => 'Alice Smith',
            'email'                 => 'alice@example.com',
            'password'              => 'secret123',
            'password_confirmation' => 'secret123',
        ]);

        $response->assertRedirect('/dashboard');
        $this->assertAuthenticated();
        $this->assertDatabaseHas('users', [
            'email' => 'alice@example.com',
            'name'  => 'Alice Smith',
        ]);
        $this->assertDatabaseMissing('users', [
            'email'    => 'alice@example.com',
            'password' => 'secret123', // password must be hashed
        ]);
    }

    public function test_duplicate_email_rejected(): void
    {
        User::factory()->create(['email' => 'alice@example.com']);

        $response = $this->post('/register', [
            'email'    => 'alice@example.com',
            'password' => 'secret123',
            'password_confirmation' => 'secret123',
        ]);

        $response->assertSessionHasErrors(['email']);
        $this->assertDatabaseCount('users', 1);
    }
}

// Unit test — no app boot, tests a single class
namespace Tests\Unit;

use PHPUnit\Framework\TestCase;
use App\Services\PriceCalculator;

class PriceCalculatorTest extends TestCase
{
    public function test_applies_discount_correctly(): void
    {
        $calculator = new PriceCalculator();
        $price = $calculator->calculate(100.00, discountPercent: 20);
        $this->assertEquals(80.00, $price);
    }

    public function test_price_never_goes_below_zero(): void
    {
        $calculator = new PriceCalculator();
        $price = $calculator->calculate(50.00, discountPercent: 150);
        $this->assertEquals(0.00, $price);
    }
}