Automated Testing in Laravel for Beginners

You add a discount feature to a checkout flow, try it in the browser, and everything works. Two days later a customer reports that orders without a discount now total incorrectly....

Automated Testing in Laravel for Beginners

You add a discount feature to a checkout flow, try it in the browser, and everything works. Two days later a customer reports that orders without a discount now total incorrectly. An old feature broke quietly because nobody checked it again. Automated tests exist to catch exactly that.

Laravel ships with everything you need for testing, so there is no setup excuse. This guide covers writing your first tests, testing the database safely, faking mail and external APIs, and the errors that most often make beginners give up.

Why Tests Are Worth Writing

  • They prevent regressions. One command rechecks dozens of scenarios in seconds.
  • They are documentation that stays true. When behavior changes, the test fails.
  • They make refactoring safe. Cleanup stops being scary.

The cost is real: tests take time up front. Start with what is most expensive to break, such as login, checkout, and anything involving money.

Feature Tests vs Unit Tests

AspectFeature testUnit test
Foldertests/Featuretests/Unit
ScopeHTTP request through response and databaseOne class or method in isolation
Framework accessFull: routing, database, sessionNone; plain PHP
SpeedSlowerVery fast
Best forUser flows: saving a product, login, checkoutPure logic: discount math, invoice number formats

Step 1: Use a Separate Test Database

Never run tests against a database with real data. In phpunit.xml, uncomment these two lines:

<env name="DB_CONNECTION" value="sqlite"/>
<env name="DB_DATABASE" value=":memory:"/>

In-memory SQLite is rebuilt on every run and never touches your main database. If your app relies on MySQL-specific features such as fulltext search, point tests at a dedicated MySQL database instead.

Step 2: Generate Test Files

php artisan make:test ProductTest          # feature test
php artisan make:test DiscountTest --unit  # unit test

The Laravel 11 installer lets you choose Pest or PHPUnit. Both run on the same engine; Pest just offers a shorter syntax. The examples use PHPUnit, with a Pest version for comparison.

Step 3: Your First Feature Tests

namespace Tests\Feature;

use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class ProductTest extends TestCase
{
    use RefreshDatabase;

    public function test_guests_are_redirected_to_login(): void
    {
        $this->get('/products')->assertRedirect('/login');
    }

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

        $this->actingAs($user)
            ->post('/products', ['name' => 'Latte', 'price' => 4.5, 'stock' => 10])
            ->assertRedirect('/products');

        $this->assertDatabaseHas('products', ['name' => 'Latte']);
    }

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

        $this->actingAs($user)
            ->post('/products', ['name' => 'Tea', 'price' => -1, 'stock' => 1])
            ->assertSessionHasErrors('price');

        $this->assertDatabaseCount('products', 0);
    }
}

RefreshDatabase migrates and resets data for every test, so test order never matters. actingAs() logs in without going through the form. The third test covers the failure path, which beginners often skip even though it is where bad input lives.

The same create test in Pest:

use App\Models\User;

uses(Illuminate\Foundation\Testing\RefreshDatabase::class);

test('user can create a product', function () {
    $this->actingAs(User::factory()->create())
        ->post('/products', ['name' => 'Latte', 'price' => 4.5, 'stock' => 10])
        ->assertRedirect('/products');

    expect(\App\Models\Product::count())->toBe(1);
});

Arrange, Act, Assert

Each test above has three parts. Arrange the state, such as creating a user with a factory. Act once, such as sending a POST. Assert the result. Separate them with blank lines. When a test starts performing several different actions, split it so a failure points at one clear cause.

Step 4: Unit Tests for Pure Logic

Suppose discount math lives in App\Support\Discount with a static apply(int $cents, int $percent): int method. Its tests need no database:

namespace Tests\Unit;

use App\Support\Discount;
use PHPUnit\Framework\TestCase;

class DiscountTest extends TestCase
{
    public function test_ten_percent_discount(): void
    {
        $this->assertSame(9000, Discount::apply(10000, 10));
    }

    public function test_discount_never_goes_below_zero(): void
    {
        $this->assertSame(0, Discount::apply(5000, 150));
    }
}

Unit tests extend PHPUnit\Framework\TestCase, not Tests\TestCase, so Laravel does not boot and they run in milliseconds. Storing money as integer cents also avoids floating-point surprises.

Step 5: Fake Mail, Queues, and External APIs

use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Mail;

Mail::fake();
Http::fake([
    'api.shipping-example.test/*' => Http::response(['cost' => 7.5], 200),
]);

// ... run the checkout request ...

Mail::assertSent(\App\Mail\InvoiceMail::class);
Http::assertSentCount(1);

The domain and mail class are placeholders. The same pattern works with Queue::fake(), Notification::fake(), and Storage::fake(), so tests never send real email or charge real cards.

Running Tests

php artisan test                       # everything
php artisan test --filter=ProductTest  # one file or test name
php artisan test --parallel            # requires brianium/paratest

Deciding What to Test First

On an existing project with no tests, covering everything at once is unrealistic. Use this priority order:

  1. Money. Totals, discounts, taxes, and balances. Mistakes here cost you directly.
  2. Access. Admin pages are closed to regular users, and one customer never sees another's data.
  3. Core flows. Sign-up, login, placing an order, generating an invoice.
  4. Past bugs. Every fixed bug gets a test so it cannot return.
  5. Everything else, added gradually whenever you touch that feature.

Do not chase 100% coverage. Ten tests guarding critical flows are worth more than hundreds checking static pages.

Run Tests on Every Push

Tests help most when nobody has to remember to run them. On GitHub, add .github/workflows/tests.yml:

name: tests
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: shivammathur/setup-php@v2
        with:
          php-version: '8.2'
      - run: composer install --no-interaction --prefer-dist
      - run: cp .env.example .env && php artisan key:generate
      - run: php artisan test

Every push and pull request is now tested, and a red cross appears before broken code reaches your server.

Common Errors and Fixes

ErrorCauseFix
no such table: productsMissing RefreshDatabaseAdd the trait to the test class
Call to undefined method factory()Model lacks HasFactoryAdd the trait to the model
Vite manifest not foundFrontend assets not builtCall $this->withoutVite() or run npm run build
Real data disappearedTests used the main databaseConfigure the test connection in phpunit.xml
Passes alone, fails in the suiteTests depend on each other's dataCreate data inside each test

Testing Habits Checklist

  • Every bug gets a failing test before the fix.
  • Every feature has at least one success test and one failure test.
  • Tests never touch production data or paid services.
  • php artisan test runs before every commit and deploy, ideally in CI.
  • Test names describe behavior, not method names.

New to building features? Start with a Laravel 11 CRUD from scratch and add tests for each action. Tests are also your safety net when customizing source code without breaking it and before deploying a Laravel app.

Yudhi
Written by
Yudhi
Founder & Lead Developer, GudangCode

Yudhi is the founder of GudangCode and a Laravel developer who has built dozens of ready-to-use business information systems — from POS and HRIS to management apps. He writes guides and articles on GudangCode to help Indonesian developers run, understand, and deploy Laravel source code correctly.

LaravelPHPMySQLSistem Informasi Bisnis See all articles by Yudhi
Want the full source code & apps?

Sign up free to download ready-to-use business applications, information systems, and Laravel source code.

Sign Up Free & Download
Testing Laravel Programming & Laravel
Read Also

Here are some related articles that might help expand your knowledge:

Share this article
Back to Blog
📚 Free Learning Hub

Learn Coding for Free at DhieCoderWeb

Explore Laravel, PHP, JavaScript tutorials, source code, web development guides, and practical programming tips.

DhieCoderWeb
100+
Tutorials
Free
Learning
SEO
Tips
Visit Dhiecoderweb.com →

Get Full Access Now!

Join our membership and unlock exclusive access to all premium features. Fast, easy, and ready to use instantly.

Join Membership Now