Hello! Welcome back to our module on Production-Ready Testing.
In our previous lesson, we established a core principle of unit testing: pure isolation. We learned to test individual methods and classes without involving external systems like the database, using mocks to fake dependencies.
Today, we'll address the scenarios where your application's logic is inherently tied to the database. It's often not enough to know that a controller method was called; you need to verify that it actually created, updated, or deleted a record correctly.
Our learning outcome for this lesson is to test database interactions using transactions and factories to ensure a clean test state. We will bridge the gap between pure unit tests and full feature tests by learning how to involve the database in a safe, clean, and repeatable way.
1. The Challenge of Database Testing
When you run tests that interact with a database, you immediately face two major problems:
- State Pollution: If Test A creates a user, Test B might fail unexpectedly because it assumed the
userstable was empty. The outcome of one test must never affect another. - Test Data Setup: To test a feature like "a user can see a paginated list of their posts," you first need to create that user and their posts in the database. Doing this manually for every test is tedious and makes the tests hard to read and maintain.
Laravel provides elegant solutions to both of these problems. Let's tackle the first one: ensuring a clean state for every test.
2. Ensuring a Clean Slate: Test Databases and Transactions
The cardinal rule of database testing is: never run tests against your development or production database. Doing so is a recipe for data corruption and unpredictable test results.
The solution is to use a dedicated database for testing. Laravel then provides a powerful mechanism to ensure this test database is reset between every single test.
Configuring a Test Database
You can configure your test database in two main ways. The most common approach for speed is to use an in-memory SQLite database, which is created and destroyed on the fly. However, since your goal is to master MSSQL, it's important to know you can also configure a separate, dedicated MSSQL database for testing. This is crucial if your application uses MSSQL-specific functions or syntax that SQLite doesn't support.
Let's watch a video that explains how to set up this configuration.
Laravel Testing 04/24: Database Configuration: RefreshDatabase, Phpunit.xml and .env.testing
This video from Laravel Daily clearly explains why you need a separate test database and demonstrates the two primary ways to configure it in Laravel.
Watch the segments from 00:00-02:03 and 03:51-05:05. Pay attention to: The explanation of why you shouldn't use your live database. How to configure the test database in phpunit.xml. The alternative method using a .env.testing file.
To summarize the video's key points:
phpunit.xml: You can uncomment the<env>variables to force theDB_CONNECTIONandDB_DATABASEfor all tests.<env name="DB_CONNECTION" value="sqlite"/> <env name="DB_DATABASE" value=":memory:"/>.env.testing: Alternatively, you can create a.env.testingfile. When you runphp artisan test, Laravel will automatically use this file instead of your regular.envfile. This is often more flexible. To use an in-memory database, your.env.testingwould look like this:
To use a dedicated MSSQL test database, you would configure it here:DB_CONNECTION=sqlite DB_DATABASE=:memory:DB_CONNECTION=sqlsrv DB_HOST=127.0.0.1 DB_PORT=1433 DB_DATABASE=my_test_database DB_USERNAME=sa DB_PASSWORD=your_password
The RefreshDatabase Trait
Simply having a separate database isn't enough. We need to ensure it's in a clean, migrated state before each test. Laravel's RefreshDatabase trait handles this brilliantly.
When you add use RefreshDatabase; to your test class, Laravel will:
- Determine the optimal way to reset the database. For most database drivers, this means it will run all your migrations once and then wrap each subsequent test inside a database transaction.
- After each test finishes, it rolls back the transaction, effectively undoing any database changes made during that test.
- This process is extremely fast and guarantees that every test starts with the same, pristine database schema.

Let's see the RefreshDatabase trait in action.
Laravel Testing 04/24: Database Configuration: RefreshDatabase, Phpunit.xml and .env.testing
We'll return to the same video, which now explains how the RefreshDatabase trait solves the problem of missing tables and ensures test isolation.
Watch from 02:07 to 03:51. The most critical takeaway is the warning about what this trait does. Understand why it's so important to have your test database properly configured before using it.
Here is how you use it in a Pest test file:
<?php
use Illuminate\Foundation\Testing\RefreshDatabase;
// This line applies the trait to all tests in this file
uses(RefreshDatabase::class);
test('a basic database interaction', function () {
// 1. Arrange: Create a user
$user = \App\Models\User::factory()->create();
// 2. Assert: Check that the user is in the database
$this->assertDatabaseHas('users', [
'email' => $user->email,
]);
});
test('the user table is empty again', function () {
// Because the transaction from the previous test was rolled back,
// the database is now empty again.
$this->assertDatabaseCount('users', 0);
});
For more detail, the official documentation provides a concise reference.
Database Testing - Resetting the Database After Each Test
The official Laravel documentation confirms the behavior of the RefreshDatabase trait and mentions a few slower alternatives.
Read the section 'Resetting the Database After Each Test'. Note the distinction between RefreshDatabase (using transactions) and the slower DatabaseMigrations or DatabaseTruncation traits.
3. Creating Test Data with Model Factories
Now that we have a clean database for each test, we need an efficient way to populate it. This is where Model Factories come in. A factory is a class that knows how to create a fake instance of an Eloquent model.
Instead of writing this in your test:
// The tedious, manual way
$product = Product::create([
'name' => 'A Test Product',
'price' => 9999, // price in cents
// ... and 10 other fields
]);
You can simply do this:
// The clean, factory way
$product = Product::factory()->create();
Factories use the Faker library to generate realistic-looking fake data (names, sentences, prices, etc.) for your model's attributes.
Let's see how to define and use them.
Laravel Testing 08/24: Factories: create many testing records
This video provides a perfect introduction to factories, showing why they are needed and how to use them to generate both single and multiple model instances.
Watch the entire video (00:00 - 04:55). It covers all the fundamentals: The problem: creating many records for a test (e.g., for pagination). Defining the rules for your model in its factory class. Using factory()->count(11)->create() to generate multiple records at once. Overriding specific attributes for a test: factory()->create(['price' => 12345]).
Key Factory Concepts
Let's put the video's lessons into a practical example.
1. Define the Factory:
You generate a factory with php artisan make:factory ProductFactory --model=Product. Then, in database/factories/ProductFactory.php, you define the default state:
// database/factories/ProductFactory.php
public function definition(): array
{
return [
'name' => fake()->words(3, true), // e.g., "dolor sit amet"
'description' => fake()->paragraph(),
'price' => fake()->numberBetween(1000, 90000), // price in cents
'stock' => fake()->numberBetween(0, 100),
];
}
2. Use the Factory in a Test:
uses(RefreshDatabase::class);
test('a product can be created', function () {
// Create one product with fake data and persist it to the DB
$product = Product::factory()->create();
$this->assertDatabaseCount('products', 1);
$this->assertDatabaseHas('products', ['name' => $product->name]);
});
test('multiple products can be created', function () {
// Create 5 products
$products = Product::factory()->count(5)->create();
$this->assertDatabaseCount('products', 5);
});
test('factory attributes can be overridden', function () {
// Create a product with a specific name and zero stock
$product = Product::factory()->create([
'name' => 'My Specific Out-of-Stock Product',
'stock' => 0,
]);
$this->assertDatabaseHas('products', [
'name' => 'My Specific Out-of-Stock Product',
'stock' => 0,
]);
});
Factories can also handle relationships, making them incredibly powerful for setting up complex test scenarios. For example, to create a user who has 3 posts:
$user = User::factory()
->has(Post::factory()->count(3))
->create();
4. Asserting the Result
Once you've used a factory to set up your 'Arrange' step and performed an 'Act' (like making an API call), you need to 'Assert' that the database is in the state you expect. Laravel provides a rich set of custom assertions for this.
Database Testing - Available Assertions
The Laravel documentation and the 'Complete Laravel Testing Guide' both provide excellent lists of available database assertions.
Scan the 'Available Assertions' section to see the variety of checks you can perform. Pay special attention to assertDatabaseHas, assertDatabaseMissing, and assertSoftDeleted.
Here are the most common ones you'll use:
$this->assertDatabaseHas('table_name', $data): Asserts that a row matching the$dataarray exists in the table.$this->assertDatabaseMissing('table_name', $data): Asserts that no row matching the$dataarray exists.$this->assertDatabaseCount('table_name', $expectedCount): Asserts that the table contains a specific number of rows.$this->assertSoftDeleted($model): Asserts that a given model instance has been soft-deleted.$this->assertModelExists($model)/$this->assertModelMissing($model): Asserts that a specific model instance exists (or doesn't) in the database.
Let's look at a complete test that uses these concepts.
// tests/Feature/Products/CreateProductTest.php
use Illuminate\Foundation\Testing\RefreshDatabase;
use App\Models\User;
uses(RefreshDatabase::class);
test('an authenticated user can create a product', function () {
// Arrange: Create an authenticated user
$user = User::factory()->create();
$this->actingAs($user);
$productData = [
'name' => 'A Brand New Gadget',
'price' => 299.99,
];
// Act: Make a POST request to the endpoint that creates products
$response = $this->postJson('/api/products', $productData);
// Assert: Check the HTTP response and the database state
$response->assertStatus(201); // Assert a "Created" status code
$this->assertDatabaseHas('products', [
'name' => 'A Brand New Gadget',
'price' => 29999, // Assuming you store price in cents
]);
});
This test cleanly demonstrates the full cycle: using RefreshDatabase for a clean slate, using a factory to create a user, acting as that user, performing an action, and finally asserting that the database was changed as expected.
Conclusion
You have now learned the essential techniques for testing database-driven features in Laravel. By combining a dedicated test database, the RefreshDatabase trait, and model factories, you can write tests that are clean, isolated, fast, and easy to read.
Key Takeaways:
- Isolate Your Test Environment: Always use a separate database for testing, configured via
phpunit.xmlor.env.testing. - Ensure a Clean State: Use the
RefreshDatabasetrait to wrap each test in a database transaction, which is automatically rolled back, ensuring no data from one test leaks into another. - Generate Data with Factories: Use
Model::factory()->create()to quickly and easily populate your test database with the records needed for your test's "arrange" step. - Assert the Outcome: Use Laravel's built-in database assertions (
assertDatabaseHas,assertDatabaseMissing, etc.) to verify that your application logic produced the correct database state.
Up Next:
In our final lesson of this module, we will explore how to use mocks and fakes to simulate dependencies and external services in tests. While we learned to test with the database today, many features also interact with other services (like sending emails, notifications, or calling third-party APIs). We'll learn how to "fake" these services so we can test our code without sending real emails or making real network requests.