2

I'm trying to work with Laravel 5.4 + PHPUnit for testing my classes. I created following class to test user controller:

<?php

namespace Tests\Feature;

use Illuminate\Foundation\Testing\DatabaseMigrations;
use Tests\TestCase;

class UserControllerTest extends TestCase
{
protected $baseUrl = 'http://localhost/pmv2';

use DatabaseMigrations;

public function testCreatesUser()
{
    echo "\nTest: POST /users => Create new user";

    $data = [
        'first_name'   => 'first_new_user',
        'last_name'    => 'last_new_user',
        'email'        => '[email protected]',
        'password'     => 'new_password',
        'phone_number' => '3333333333',
        'status'       => 'active',
        'created_at'   => '2000-1-1 10:10:00',
        'updated_at'   => '2000-1-1 10:10:00',
    ];

    $response = $this->post('/users', $data);
    $response->assertStatus(200);

    $this->assertDatabaseHas('users', ['email' => $data['email']]);
}

public function testReadAllUsers()
{
    $this->seed('UsersTableSeeder');

    echo "\nTest: GET /users => Read all users";

    $this->seed('UsersTableSeeder');

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

    $response->assertStatus(200);
    $response->assertJson([
        'found' => true,
        'users' => [],
    ]);
}

public function testReadSingleUser()
{
    $this->seed('UsersTableSeeder');

    echo "\nTest: POST /users => Read single user";

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

    $response->assertStatus(200);
    $response->assertJson([
        'found' => true,
        'user'  => [],
    ]);

}

public function testUpdateUser()
{
    $this->seed('UsersTableSeeder');

    echo "\nTest: POST /users => Create new user";

    $data = [
        'first_name'   => 'first_updated_user',
        'last_name'    => 'last_updated_user',
        'email'        => '[email protected]',
        'password'     => 'updated_password',
        'phone_number' => '44444444444',
        'updated_at'   => '2000-1-1 10:10:00',
    ];

    $response = $this->put('/users/1', $data);

    $response->assertStatus(200);
    $this->assertDatabaseHas('users', ['email' => $data['email']]);
}
}

The problem here is that database is refreshed for every single test. I need to refresh the migration only once before the very first test runs and after the last test.

3 Answers 3

3

That's not a good idea: if you need this it means that your tests aren't independent and even the order of execution can afflict final result.

That's not good: you should follow FIRST principles

  • F ast
  • I ndependent / I solated
  • R repeatable
  • S elf validating
  • T imely

Read more here

Sign up to request clarification or add additional context in comments.

Comments

2

What happens when a user is created? For example that exists one more user in database. For example you can count users before and after the POST calls.

You dont really need to test POST, but what happens when POST calls is committed.

And you dont test that a /user/{id} exists. This make test dependent to an id: tests MUST be independent. You can put in the same test a POST (to create the user), get the last id from the response or from the database and then GET that user. This is a way to check that exactly same user is in the database. And is a way to remove the id. If you depend on ids, the test run only once. Your tests must be repeatable.

Reset database each time costs too much. I suggest you to do the following steps:

1) recreate database with fixtures (just what you need for tests) 2) run all tests

Remember also that tests must be fast. A test suite with a duration over 15/20 minutes is slow! You need to be fast.

Reading test testReadSingleUser, I suggest you to create the user "by hand", retrieve the id, finally to GET the user with that id. This makes the test repeatable infinite times.

Comments

0

This can be accomplished by running your database migrations manually within the test case in place of using the database testing traits provided by Laravel. Additionally, PHPUnit provides the Depends attribute to allow you to explicitly declare which tests require previous tests' results and pass information between them.

As an example, I will assume the existence of two resources, A and B. Resource A can be created on its own, but resource B requires an ID to an existing instance of A, perhaps because it is a foreign key on its model. Rather than re-creating a new resource of A in the test case for B, an ID for the A resource can be passed over to the dependent test case.

<?php
namespace Tests\Feature;

use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\TestDox;
use PHPUnit\Framework\Attributes\Depends;
use Tests\TestCase;

class MyFeatureTest extends TestCase
{
    #[Test]
    #[TestDox('Create resource A')]
    public function CreateResourceA(): int
    {
        // Set the database to a fresh state
        $this->artisan('migrate:fresh');

        // Create resource A
        $response = $this->putJson('/api/a/create', ['content']);
        $response->assertStatus(200);

        // Get the id of the created A resource
        $resourceA = json_decode($response->getContent());
        $id = $resourceA['id'];

        return $id;
    }

    #[Test]
    #[TestDox('Create resource B')]
    #[Depends('CreateResourceA')]
    public function CreateResourceB(int $id)
    {
        // Create a resource B that requires an existing id of resource A which has been passed from CreateResourceA
        $response = $this->putJson('/api/b/create', ['resource_a_id' => $id]);
        $response->assertStatus(200);
    }
}

Note that persisting data between tests like this also requires a persistent database. Notably, using sqlite with a :memory: database will not work as the database will be closed and reopened between test cases, resulting in a blank database in the case of :memory: being used. If you have to go this route, consider creating an empty sqlite database file dedicated to your testing purposes. Your database-driven tests will become much slower to run as a natural consequence of this.

The other answers on this question already go over why you should avoid doing this, but also didn't answer the question. I've written this up since I feel like it is important to still have a solution for this, even if it not best practice. After all, while best practices are often good suggestions; those suggestions don't always survive contact with real world use cases.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.