Testing PHP Code (When You Really Don't Want To)

matt
Matthew Gros · Jan 10, 2026

TLDR

Test the stuff that matters, use factories for data, mock external services, run tests before deploying.

Testing PHP Code (When You Really Don't Want To)

Why Bother Testing?

Writing tests feels like extra work until:

  • You break something in production that tests would have caught
  • You refactor and have no idea if you broke anything
  • You come back to code after 3 months and forgot what it does

Tests are documentation that runs.

PHPUnit Basics

Create a test file:

// tests/Unit/CartTest.php
namespace Tests\Unit;

use PHPUnit\Framework\TestCase;
use App\Cart;

class CartTest extends TestCase
{
    public function test_empty_cart_has_zero_total(): void
    {
        $cart = new Cart();

        $this->assertEquals(0, $cart->total());
    }

    public function test_cart_calculates_item_totals(): void
    {
        $cart = new Cart();
        $cart->add(['name' => 'Widget', 'price' => 1000, 'quantity' => 2]);

        $this->assertEquals(2000, $cart->total());
    }
}

Run them:

php artisan test
# or
./vendor/bin/phpunit

Name Tests Like Documentation

Good test names tell you what broke without reading the code:

// Good - I know exactly what failed
public function test_user_cannot_checkout_with_empty_cart(): void
public function test_discount_code_applies_percentage_off(): void
public function test_shipping_is_free_for_orders_over_fifty(): void

// Bad - tells me nothing
public function testCheckout(): void
public function testDiscount(): void

Testing Laravel Controllers

Feature tests hit your actual routes:

namespace Tests\Feature;

use Tests\TestCase;
use App\Models\User;
use App\Models\Post;

class PostTest extends TestCase
{
    public function test_guests_cannot_create_posts(): void
    {
        $response = $this->post('/posts', [
            'title' => 'My Post',
            'content' => 'Content here'
        ]);

        $response->assertRedirect('/login');
    }

    public function test_users_can_create_posts(): void
    {
        $user = User::factory()->create();

        $response = $this->actingAs($user)->post('/posts', [
            'title' => 'My Post',
            'content' => 'Content here'
        ]);

        $response->assertRedirect('/posts');
        $this->assertDatabaseHas('posts', [
            'title' => 'My Post',
            'user_id' => $user->id
        ]);
    }
}

Test Validation

public function test_post_title_is_required(): void
{
    $user = User::factory()->create();

    $response = $this->actingAs($user)->post('/posts', [
        'content' => 'Just content, no title'
    ]);

    $response->assertSessionHasErrors('title');
}

Factories Make Test Data Easy

// database/factories/PostFactory.php
class PostFactory extends Factory
{
    public function definition(): array
    {
        return [
            'title' => fake()->sentence(),
            'content' => fake()->paragraphs(3, true),
            'user_id' => User::factory(),
        ];
    }

    public function published(): static
    {
        return $this->state(['published_at' => now()]);
    }
}

Use them in tests:

$post = Post::factory()->create();
$publishedPosts = Post::factory()->published()->count(5)->create();

Mocking External Services

Don't hit real APIs in tests:

public function test_weather_widget_displays_temperature(): void
{
    Http::fake([
        'api.weather.com/*' => Http::response(['temp' => 72], 200)
    ]);

    $response = $this->get('/dashboard');

    $response->assertSee('72');
}

Laravel has fakes for Mail, Queue, Storage, etc:

public function test_welcome_email_is_sent(): void
{
    Mail::fake();

    $this->post('/register', [
        'email' => 'test@example.com',
        'password' => 'password123'
    ]);

    Mail::assertSent(WelcomeEmail::class);
}

What to Actually Test

Test the important stuff:

  • Business logic (calculations, state changes)
  • Things that broke before
  • Edge cases (empty inputs, big numbers, special characters)
  • Security stuff (auth, permissions)

Skip testing:

  • Simple getters/setters
  • Framework code
  • Third-party packages

Make Tests Part of Your Workflow

Run tests before committing:

# In your pre-commit hook or CI
php artisan test --parallel

Broken tests should block deployment. Period.

Start Small

You don't need 100% coverage. Start with:

  1. Tests for your most critical features
  2. A test for every bug you fix (so it doesn't come back)
  3. Tests for complex logic

Add more over time. Some tests are infinitely better than no tests.

About the Author

matt

I build and ship automation-driven products using Laravel and modern frontend stacks (Vue/React), with a focus on scalability, measurable outcomes, and tight user experience. I’m based in Toronto, have 13+ years in PHP, and I also hold a pilot’s license. I enjoy working on new tech projects and generally exploring new technology.