Testing commands — artisan() in feature tests
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. UseRefreshDatabase. - 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
// 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);
}
}