Courses

Testing in Laravel 12 For Beginners

DB For Testing: RefreshDatabase, phpunit.xml, .env.testing

Summary of this lesson:
- Configuring separate testing database environment
- Setting up SQLite for testing
- Understanding RefreshDatabase trait
- Managing test environment with phpunit.xml and .env.testing

This lesson will be a very important one. If you want to avoid accidentally wiping out your database data, carefully read this one.

As you saw in the previous lesson, we interact with the database, creating some "fake" data for testing, kinda like simulating the database.

For that, we must be very careful: our tests shouldn't interact with live databases. We need to configure a separate testing database where our tests would run.


When creating a new Laravel project, you can choose the testing framework. However, the configuration is made in the phpunit.xml file for both frameworks.

The database configuration is in two variables, DB_CONNECTION and DB_DATABASE, which are commented out by default.

phpunit.xml:

<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="./vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true"
>
<testsuites>
<testsuite name="Unit">
<directory>tests/Unit</directory>
</testsuite>
<testsuite name="Feature">
<directory>tests/Feature</directory>
</testsuite>
</testsuites>
<source>
<include>
<directory>app</directory>
</include>
</source>
<php>
<env name="APP_ENV" value="testing"/>
<env name="APP_MAINTENANCE_DRIVER" value="file"/>
<env name="BCRYPT_ROUNDS" value="4"/>
<env name="CACHE_STORE" value="array"/>
<!-- <env name="DB_CONNECTION" value="sqlite"/> -->
<!-- <env name="DB_DATABASE" value=":memory:"/> -->
<env name="MAIL_MAILER" value="array"/>
<env name="PULSE_ENABLED" value="false"/>
<env name="QUEUE_CONNECTION" value="sync"/>
<env name="SESSION_DRIVER" value="array"/>
<env name="TELESCOPE_ENABLED" value="false"/>
</php>
</phpunit>

So, uncomment those lines and specify the connection and the database where your tests should be run. You can check what connections are available in the config/database.php. For example, SQLite, mysql.

IMPORTANT: if you don't do that, any DB operations in your tests will execute on the LIVE database. So, please configure this BEFORE writing any tests.

In most cases, running tests using the SQLite connection and memory as the database is pretty safe. In this case, you wouldn't need to create a separate database with SQL client manually.

The Exception is if you use specific MySQL functions that SQLite doesn't support. For that case, manually create a separate MySQL database, call it something like db_testing, and configure its connection in the config/database.php.

In our case, we will proceed with SQLite. Uncomment those two lines in the phpunit.xml and re-run the tests. Now, tests are run in a separate database. By default, Pest has added the RefreshDatabase trait for all feature tests to run migrations after every test.

tests/Pest.php:

use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
 
uses(TestCase::class, RefreshDatabase::class)->in('Feature');
 
// ...

Yes, that means that every time we run php artisan test, Pest will clean all the database and re-run migrations from scratch. This is precisely why you should use a separate database to avoid wiping the data in your main database.

Run the tests. They should be green.

tests sqlite green

If you look in the products table on your live database, you will see that no new records were added.


PHPUnit examples

After setting up the testing database and running the tests, we will get an error no such table: products.

no table error

And of course. We specified a new connection and database but have not run the migrations. We must add the RefreshDatabase trait to every test class. This trait will re-run migration every time this test is run.

tests/Feature/ProductsTest.php:

use Illuminate\Foundation\Testing\RefreshDatabase;
 
class ProductsTest extends TestCase
{
use RefreshDatabase;
 
// ...
}

In addition to phpunit.xml, there is another way to configure the env variables. Like in the .env file, you can create a separate .env.testing file.

Comment out the DB configuration in the phpunit.xml file. Create a new file .env.testing with the same content as in .env.

Now we can change the DB connection and database in the .env.testing. When the tests are run, the DB connection settings will be taken from .env.testing.

.env.testing:

APP_NAME=Laravel
APP_ENV=testing [tl! highlight]
APP_KEY=base64:Oo7mTQpSW00WmjWs1kJVjqFZw/oQVENUxyhuyLCQgfk=
APP_DEBUG=true
APP_URL=http://localhost
 
LOG_CHANNEL=stack
LOG_LEVEL=debug
 
DB_CONNECTION=sqlite
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=:memory:
DB_USERNAME=root
DB_PASSWORD=
 
// ...

It's your preference whether to use phpunit.xml or .env.testing.


So, this is how you configure the database for performing operations inside the testing methods.

avatar

I think it's php artisan test instead of php artisan migrate in this sentence : << Yes, that means that every time we run php artisan migrate, Pest will clean all the database and re-run migrations from scratch. >>

avatar

Thank you for reporting this, it's fixed now!

avatar

.env.testing doesn't work in the Laravel Zero. Only phpunit.xml configuration influence the testing

avatar

Could you tell me how to use Elasticsearch for testing properly? Launch two containers and provide the details of the second one in .env.testing?

avatar

These test won't run because in the last lessons you used breeze which requires authentication. and /products page is for the user which is authenticated. you have to use actingAs: https://pestphp.com/docs/plugins#content-laravel:~:text=use%20function%20Pest%5CLaravel%5C%7BactingAs%7D%3B

avatar

Hi there, I am struggling with database transactions. In the actual code, the transaction is working fine. But when I visit the route form the test, the database transaction does not work at all. I tried all alternatives even testing on the same database of the application. It is an API, when I visit the route from postman, it works fine, but from the test, it always fails although I am using Illuminate\Foundation\Testing\RefreshDatabase::class trait. Here is the code:

public function store(StoreRequestRequest $request, StoreRequestAction $action): JsonResource
{
    $insertedWorkflowRequest = DB::transaction(function () use ($request, $action) {
        $workflowRequest = $action->handle(Auth::user(), $request->validated());
        (new StoreStepsAction($workflowRequest))->handle();

        return $workflowRequest;
    });

    $firstStep = $insertedWorkflowRequest->load('steps')->steps->first();

    $approvers = (new StepApproverUserService($firstStep))->handle();
    Mail::to($approvers)->send(new WorkflowRequestMail($insertedWorkflowRequest));

    return new RequestResource($insertedWorkflowRequest);
}

And the test:

test('authenticated user can create a request', function () {
    $this->seed([UserSeeder::class, WorkflowSeeder::class]);
    $user = User::factory()->create();
    $response = $this->actingAs($user)->postJson(route('requests.store'), [
        'workflow_id' => Workflow::first()->id,
    ]);

    $response->assertStatus(201);
    expect($response['status'])->toBe(Status::PENDING->value);
    expect($response['priority'])->toBe(Priority::LOW->value);
    expect($response['data'])->toBeNull();
});
	
	

I know there is no test for the mail. I'll do it later or separtely.

Note: My last resort was to create a unit test for the action and it is working separtely, not in the transaction.

avatar

hi, what exactly here fails for you?

avatar

The database transaction does not work in the test. As much as I am concerned, the test refreshes the database once the test finishes not after every commit. For example, we have three steps inside the transaction, the database should be refresed after the three steps has been commited (based on my understanding). But it seems that the database refreshes after each step. In other words: database refreshes after step 1. As a result when the test tries to assert a record created by step2, it does not find it.

Therefore, my conclusion (which is supported by answer by chatgpt) is that database transactions cannot be tested. The last resort I did is to unit test each step separtelly.

I hope I can express my thoughts thoughroughly

avatar

There is a little flaw in the design of the system in that case. The Database Transaction should be surrounding all database actions and not just the initial creation. Imagine you have a scenario - your database fails to create new record, but you still try to do actions with it. That's not going to work - since the record creation failed.

And this is the behaviour you are seeing in your test - database transaction might be failing in your case due to whatever reason, but you don't see the reason and all you get is - failed test.

Try a few things:

  1. You can try to remove the DB::Transaction for a single run to see if everything still works. Maybe you got an Exception that is invisible
  2. Try to surround the $firstStep and $approvers within the transaction - see how that works.

And lastly, I would assume that you technically have Action pattern here, but just in case - what is your QUEUE_DRIVER set to in your .env.testing ? :)