Database Testing

Giới thiệu

Laravel cung cấp nhiều công cụ hỗ trợ để bạn có thể kiểm tra dữ liệu trong Database hoặc là đôi khi bạn cần tạo ra các dữ liệu mẫu để test các chức năng hay các functions. Bài viết này sẽ giúp bạn giải đáp các thắc mắc đó.

Đầu tiên, bạn có thể sử dụng assertDatabaseHas helper để xác nhận rằng dữ liệu tồn tại trong database phù hợp với giá trị đưa vào hay không. Ví dụ, nếu bạn muốn kiểm tra rằng đó là một bản ghi trong bảng users với giá trị emailsally@example.com, bạn có thể làm như sau:

public function testDatabase()
{
    // Make call to application...

    $this->assertDatabaseHas('users', [
        'email' => 'sally@example.com',
    ]);
}

Bạn cũng có thể sử dụng assertDatabaseMissing helper để xác nhận rằng dữ liệu đó không tồn tại trong database.

Phương thức assertDatabaseHas và các helper khác giống nó cũng rất hay được sử dụng. Bạn có thể tự do sử dụng bất kỳ phương phức xác nhận nào tích hợp tong PHPUnit để hỗ trợ cho các feature test của bạn.

Tạo các Factory

Để tạo một factory, sử dụng command make:factory

php artisan make:factory PostFactory

Một factory mới được đặt trong thư mục database/factories

Tùy chọn --model có thể được sử dụng để đề cử tên của model được tạo ra bởi factory. Tùy chọn này sẽ điền trước vào file factoy được sinh ra với model nhận vào:

php artisan make:factory PostFactory --model=Post

Reset database sau mỗi lần test

Thông thường, bạn sẽ cần reset lại database sau mỗi lần test để dữ liệu từ lần test trước không ảnh hưởng tới các lần test tiếp theo (subsequent). RefreshDatabase trait là một cách hiệu quả nhất để thay đổi database test của bạn tùy thuộc vào bạn sử dụng một database trong bộ nhớ hoặc database truyền thống. Sử dụng trait trong class test của bạn và mọi thứ sẽ được xử lý cho bạn:

<?php

namespace Tests\Feature;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Tests\TestCase;

class ExampleTest extends TestCase
{
    use RefreshDatabase;

    /**
     * A basic functional test example.
     *
     * @return void
     */
    public function testBasicExample()
    {
        $response = $this->get('/');

        // ...
    }
}

Viết các Factory

Khi viết test, bạn có thể cần insert một vài bản ghi vào database trước khi thực thi test. Thay vì ghi rõ từng giá trị của mỗi cột khi bạn tạo dữ liệu test này, Laravel cho phép bạn định nghĩa một cài đặt mặc định cho các thuộc tính cho mỗi models của bạn sử dụng các factory model. Để bắt đầu, nhìn vào file database/factories/UserFactory.php trong ứng dụng của bạn. Ngoài ra, file này cũng chứa một định nghĩa factory:

use Faker\Generator as Faker;
use Illuminate\Support\Str;

$factory->define(App\User::class, function (Faker $faker) {
    return [
        'name' => $faker->name,
        'email' => $faker->unique()->safeEmail,
        'email_verified_at' => now(),
        'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
        'remember_token' => Str::random(10),
    ];
});

Trong Closure này, phần định nghĩa factory, bạn có thể trả về các giá trị mặc định cho toàn bộ các thuộc tính trong model. Closure này sẽ nhận một instance của thư viện Faker PHP, cái mà cho phép bạn tạo ra các giá trị ngẫu nhiên cho việc test (Cái này rất hữu ích khi bạn cần dữ liệu trong DB để chạy test =))).

Bạn có thể tạo các file factory bổ sung cho mỗi model để tổ chức tốt hơn. Ví dụ, bạn có thể tạo file UserFactory.phpCommentFactory.php trong thư mục database/factories. Toàn bộ các file trong thư mục factories sẽ tự động được load bởi Laravel.

Bạn có thể đặt Faker locale bằng cách thêm tùy chọn faker_locale vào file cấu hình config/app.php của bạn.

Mở rộng các factory

Nếu bạn đã extend model, bạn có thể muốn extend factory của nó như là để tận dụng các thuộc tính factory của model con trong khi testing và seeding. Để đạt được điều này, bạn có thể gọi phương thức raw của factory builder để có được raw array của các thuộc tính từ bất kỳ factory nào:

$factory->define(App\Admin::class, function (Faker\Generator $faker) {
    return factory(App\User::class)->raw([
        // ...
    ]);
});

Factory state

Các state cho phép bạn định nghĩa các sửa đổi riêng biệt mà có thể được áp dụng vào các model factory trong bất kỳ sự kết hợp nào. Ví dụ, model user của bạn có thể có một delinquent state mà sửa đổi một trong các giá trị thuộc tính của nó. Bạn có thể định nghĩa các state biến đổi của bạn sử dụng phương thức state. Đối với các state đơn giản, bạn có thể truyển vào một array các sửa đổi:

$factory->state(App\User::class, 'delinquent', [
    'account_status' => 'delinquent',
]);

Nếu state của bạn yêu cầu tính toán hoặc một đối tượng $faker, Bạn có thể sử dụng một Closure để tính toán các thuộc tính state sửa đổi:

$factory->state(App\User::class, 'address', function ($faker) {
    return [
        'address' => $faker->address,
    ];
});

Các Factory callback

Các Factory callback được đăng ký sử dụng phương thức afterMakingafterCreateing, cho phép bạn thực hiện các nhiệm vụ sau khi chuẩn bị tạo hoặc đang tạo một model. Ví dụ, bạn có thể sử dụng các callback để liên kết bổ sung các model với model đã được tạo:

$factory->afterMaking(App\User::class, function ($user, $faker) {
    // ...
});

$factory->afterCreating(App\User::class, function ($user, $faker) {
    $user->accounts()->save(factory(App\Account::class)->make());
});

Bạn cũng có thể định nghĩa các callback cho factory state

Sử dụng các Factory

Tạo model

Một khi bạn đã định nghĩa được các factory của bạn, bạn có thể sử dụng hàm global factory trong các feature test hoặc trong các file seed để tạo các model instance. Vậy, hãy nhìn vào một vài ví dụ về việc tạo các model. Đầu tiên, chúng ta sẽ dùng phương thức make để tạo các model nhưng không lưu chúng vào database:

public function testDatabase()
{
    $user = factory(App\User::class)->make();

    // Use model in tests...
}

Bạn cũng có thể tạo một Collection với nhiều model hoặc tạo các model với một loại nhất định:

// Create three App\User instances...
$users = factory(App\User::class, 3)->make();

Áp dụng các state

Bạn có thể áp dụng bất kỳ state nào của bạn vào trong các model. Nếu bạn muốn áp dụng nhiều các state biến đổi vào các model, bạn nên đặt tên cụ thể cho mỗi state bạn muốn áp dụng:

$users = factory(App\User::class, 5)->states('delinquent')->make();

$users = factory(App\User::class, 5)->states('premium', 'delinquent')->make();

Ghi đè các thuộc tính

Nếu bạn muốn ghi đè một vài giá trị mặc định trong model của bạn, bạn có thể truyền một mảng các giá trị vào phương thức make. Chỉ khi các giá trị được chỉ định cụ thể sẽ được thay thế trong khi phần còn lại của các giá trị được đặt bởi factory:

$user = factory(App\User::class)->make([
    'name' => 'Abigail',
]);

Persisting Models

Phương thức create không chỉ tạo các model instance mà còn lưu chúng vào database sử dụng phương thức save của Eloquent

public function testDatabase()
{
    // Create a single App\User instance...
    $user = factory(App\User::class)->create();

    // Create three App\User instances...
    $users = factory(App\User::class, 3)->create();

    // Use model in tests...
}

Bạn cũng có thể ghi đè các thuộc tính trong model bằng cách truyền vào một array đến phương thức create:

$user = factory(App\User::class)->create([
    'name' => 'Abigail',
]);

Relations & Attribute Closures

Bạn cũng có thể gắn các relationship vào các model trong khi định nghĩa các factory. Ví dụ, nếu bạn muốn tạo một đối tượng User khi đăng tạo một Post, bạn có thể làm như sau

$factory->define(App\Post::class, function ($faker) {
    return [
        'title' => $faker->title,
        'content' => $faker->paragraph,
        'user_id' => factory(App\User::class),
    ];
});

Nếu relationship phụ thuộc bên trong factory mà nó định nghĩa bạn có thể cung cấp một callback mà nhận mảng thuộc tính:

$factory->define(App\Post::class, function ($faker) {
    return [
        'title' => $faker->title,
        'content' => $faker->paragraph,
        'user_id' => factory(App\User::class),
        'user_type' => function (array $post) {
            return App\User::find($post['user_id'])->type;
        },
    ];
});

Sử dụng các seed

Nếu bạn muốn sử dụng database seeders để điền vào database trong một feature test, bạn có thể sử dụng phương thức seed. Mặc định, phương thức seed sẽ trả về DatabaseSeeder , cái mà nên thực thi toàn bộ các seeder khác của bạn. Hoặc là, bạn truyền vào tên một class seeder cụ thể đến phương thức seed:

<?php

namespace Tests\Feature;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use OrderStatusesTableSeeder;
use Tests\TestCase;

class ExampleTest extends TestCase
{
    use RefreshDatabase;

    /**
     * Test creating a new order.
     *
     * @return void
     */
    public function testCreatingANewOrder()
    {
        // Run the DatabaseSeeder...
        $this->seed();

        // Run a single seeder...
        $this->seed(OrderStatusesTableSeeder::class);

        // ...
    }
}

Các Assertion

Laravel cung cấp một vài xác nhận database cho các PHPUnit feature test

Method Description
$this->assertDatabaseHas($table, array $data); Xác nhận rằng một bảng chứa dữ liệu nhận vào.
$this->assertDatabaseMissing($table, array $data); Xác nhận rằng một bảng không chứa dữ liệu nhận vào.
$this->assertDeleted($table, array $data); Xác nhận rằng bản ghi đưa vào đã bị xóa.
$this->assertSoftDeleted($table, array $data); Xác nhận rằng bản ghi đưa vào bị xóa tạm thời.

Để cho tiện, bạn có thể truyền vào một model đến các helper
assertDeletedassertSoftDeleted để xác nhận rằng bản ghi đã bị xóa hay bị xóa tạm thời, tương ứng, từ database dựa trên khóa chính của model.

Ví dụ, nếu bạn đang sử dụng một model factory trong test của bạn, bạn có thể truyền model này để đến một trong các helper để test ứng dụng của bạn đã xóa đúng bản ghi từ database :

public function testDatabase()
{
    $user = factory(App\User::class)->create();

    // Make call to application...

    $this->assertDeleted($user);
}