데이터베이스 테스팅

시작하기

라라벨은 데이터베이스 기반 애플리케이션을 더 쉽게 테스트할 수 있도록 다양한 유용한 도구와 검증을 제공합니다. 또한 라라벨 모델 팩토리와 시더를 사용하면 애플리케이션의 Eloquent 모델과 관계를 사용하여 테스트 데이터베이스 레코드를 손쉽게 생성할 수 있습니다. 다음 문서에서 이러한 모든 강력한 기능에 대해 설명합니다.

각각의 테스트 수행 후에 데이터베이스 재설정하기

더 진행하기 전에 이전 테스트의 데이터가 후속 테스트를 방해하지 않도록 각 테스트 후에 데이터베이스를 재설정하는 방법에 대해 논의하겠습니다. 라라벨에 포함된 Illuminate\Foundation\Testing\RefreshDatabase 특성-trait이 이 문제를 처리합니다. 테스트 클래스에서 특성-trait을 사용하기만 하면 됩니다.

<?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 test_basic_example()
    {
        $response = $this->get('/');

        // ...
    }
}

모델 팩토리 정의

개념 설명

먼저 Eloquent 모델 팩토리에 대해 이야기해 보겠습니다. 테스트할 때 테스트를 실행하기 전에 데이터베이스에 몇 개의 레코드를 삽입해야 할 수도 있습니다. 이 테스트 데이터를 생성할 때 각 열의 값을 수동으로 지정하는 대신 라라벨을 사용하면 모델 팩토리를 사용하여 Eloquent 모델 각각에 대한 기본 속성 세트를 정의할 수 있습니다.

팩토리를 작성하는 방법의 예제를 보려면 애플리케이션의 database/factories/UserFactory.php 파일을 살펴보세요. 이 팩토리는 새로만든 모든 라라벨 애플리케이션에 포함되어 있으며 다음과 같은 팩토리 정의를 포함하고 있습니다.

namespace Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;

class UserFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array
     */
    public function definition()
    {
        return [
            'name' => $this->faker->name(),
            'email' => $this->faker->unique()->safeEmail(),
            'email_verified_at' => now(),
            'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
            'remember_token' => Str::random(10),
        ];
    }
}

보시다시피 가장 기본적인 형태의 팩토리는 라라벨의 기본 팩토리 클래스를 확장하고 definition 메소드를 정의하는 클래스입니다. definition 메서드는 팩토리를 사용하여 모델을 생성할 때 적용해야 하는 기본 속성 값 집합을 반환합니다.

팩토리는 faker 속성을 통해 Faker PHP 라이브러리에 액세스할 수 있으므로 테스트를 위해 다양한 종류의 임의 데이터를 편리하게 생성할 수 있습니다.

{tip} config/app.ph1p 설정 파일에 faker_locale 옵션을 추가하여 애플리케이션의 Faker 로케일을 설정할 수 있습니다.

팩토리 생성하기

팩토리를 생성하려면 make:factory Artisan command를 실행하세요.

php artisan make:factory PostFactory

새 팩토리 클래스는 database/factories 디렉토리에 생성됩니다.

모델 및 팩토리 연동 규칙

팩토리를 정의한 후에는, 해당 모델에 대한 팩토리 인스턴스를 인스턴스화하기 위해 Illuminate\Database\Eloquent\Factories\HasFactory 트레잇에 의해 모델에 제공되는 정적 factory 메소드를 사용할 수 있습니다.

HasFactory 트레잇의 factory 메소드는 규칙을 사용하여 트레잇이 할당된 모델에 대한 적절한 팩토리를 결정합니다. 특히 이 메서드는 모델 이름과 일치하는 클래스 이름이 있고 접미사가 FactoryDatabase\Factories 네임스페이스의 팩토리를 찾습니다. 이러한 규칙이 특정 애플리케이션이나 팩토리에 적용되지 않는 경우 모델의 newFactory 메소드를 덮어써서 모델의 해당 팩토리 인스턴스를 직접 반환할 수 있습니다.

use Database\Factories\Administration\FlightFactory;

/**
 * Create a new factory instance for the model.
 *
 * @return \Illuminate\Database\Eloquent\Factories\Factory
 */
protected static function newFactory()
{
    return FlightFactory::new();
}

다음으로 해당 팩토리에서 model 속성을 정의합니다.

use App\Administration\Flight;
use Illuminate\Database\Eloquent\Factories\Factory;

class FlightFactory extends Factory
{
    /**
     * The name of the factory's corresponding model.
     *
     * @var string
     */
    protected $model = Flight::class;
}

팩토리 상태(States)

상태 조작 방법을 사용하면 어떤 조합으로든 모델 팩토리에 적용할 수 있는 개별 수정을 정의할 수 있습니다. 예를 들어, Database\Factories\UserFactory 팩토리에는 기본 속성 값 중 하나를 수정하는 suspended 상태 메소드가 포함될 수 있습니다.

상태 변환 메소드는 일반적으로 라라벨의 기본 팩토리 클래스에서 제공하는 state 메소드를 호출합니다. state 메소드는 팩토리에 대해 정의된 원시 속성의 배열을 수신하고 수정할 속성의 배열을 반환해야 하는 클로저를 입력받습니다.

/**
 * Indicate that the user is suspended.
 *
 * @return \Illuminate\Database\Eloquent\Factories\Factory
 */
public function suspended()
{
    return $this->state(function (array $attributes) {
        return [
            'account_status' => 'suspended',
        ];
    });
}

팩토리 콜백

팩토리 콜백은 afterMakingafterCreating 메소드를 사용하여 등록되며 모델을 만들거나 생성한 후 추가 작업을 수행할 수 있습니다. 팩토리 클래스에서 configure 메소드를 정의하여 이러한 콜백을 등록해야 합니다. 이 메소드는 팩토리가 인스턴스화될 때 라라벨에 의해 자동으로 호출됩니다.

namespace Database\Factories;

use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;

class UserFactory extends Factory
{
    /**
     * Configure the model factory.
     *
     * @return $this
     */
    public function configure()
    {
        return $this->afterMaking(function (User $user) {
            //
        })->afterCreating(function (User $user) {
            //
        });
    }

    // ...
}

팩토리를 사용하여 모델 생성

모델 인스턴스화

팩토리를 정의한 후에는 해당 모델에 대한 팩토리 인스턴스를 인스턴스화하기 위해 Illuminate\Database\Eloquent\Factories\HasFactory 트레잇에 의해 모델에 제공되는 정적 factory 메소드를 사용할 수 있습니다. 모델 생성의 몇 가지 예를 살펴보겠습니다. 먼저, 모델을 데이터베이스에 유지하지 않고 생성하기 위해 make 메소드를 사용할 것입니다.

use App\Models\User;

public function test_models_can_be_instantiated()
{
    $user = User::factory()->make();

    // Use model in tests...
}

count 메소드를 사용하여 여러 모델의 컬렉션을 만들 수 있습니다.

$users = User::factory()->count(3)->make();

상태 적용

states를 모델에 적용할 수도 있습니다. 여러 상태 변환을 모델에 적용하려면 상태 변환 메서드를 직접 호출하면 됩니다.

$users = User::factory()->count(5)->suspended()->make();

속성 재지정(오버라이딩) 하기

모델의 기본값 중 일부를 재정의하려면 make 메서드에 값 배열을 전달할 수 있습니다. 지정된 속성만 교체되고 나머지 속성은 공장에서 지정한 기본값으로 설정된 상태로 유지됩니다.

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

또는 팩토리 인스턴스에서 state 메소드를 직접 호출하여 인라인 상태 변환을 수행 할 수 있습니다.

$user = User::factory()->state([
    'name' => 'Abigail Otwell',
])->make();

{tip} 대량 할당 보호는 팩토리를 사용하여 모델을 생성할 때는 자동으로 비활성화됩니다.

모델 저장하기

create 메소드는 모델 인스턴스를 인스턴스화하고 Eloquent의 save 메소드를 사용하여 데이터베이스에 저장합니다.

use App\Models\User;

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

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

    // Use model in tests...
}

속성 배열을 create 메소드에 전달하여 팩토리의 기본 모델 속성을 재정의할 수 있습니다.

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

시퀀스

때로는 생성된 각 모델에 대해 주어진 모델 속성의 값을 대체하고자 할 수 있습니다. 상태 변환을 시퀀스로 정의하여 이를 수행할 수 있습니다. 예를 들어, 생성된 각 사용자에 대해 admin 열의 값을 YN으로 대체할 수 있습니다.

use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Sequence;

$users = User::factory()
                ->count(10)
                ->state(new Sequence(
                    ['admin' => 'Y'],
                    ['admin' => 'N'],
                ))
                ->create();

이 예에서는 admin 값이 Y인 5명의 사용자가 생성되고 admin 값이 N인 5명의 사용자가 생성됩니다.

필요한 경우 클로저를 시퀀스 값으로 포함할 수 있습니다. 시퀀스에 새 값이 필요할 때마다 클로저가 호출됩니다.

$users = User::factory()
                ->count(10)
                ->state(new Sequence(
                    fn ($sequence) => ['role' => UserRoles::all()->random()],
                ))
                ->create();

시퀀스 클로저 내에서 클로저에 주입된 시퀀스 인스턴스의 $index 또는 $count 속성에 액세스할 수 있습니다. $index 속성은 지금까지 발생한 시퀀스의 반복 횟수를 포함하고, $count 속성은 시퀀스가 호출될 총 횟수를 포함합니다.

$users = User::factory()
                ->count(10)
                ->sequence(fn ($sequence) => ['name' => 'Name '.$sequence->index])
                ->create();

팩토리 관계

1:*(일대다) 관계

다음으로, 라라벨의 유창한 팩토리 메서드를 사용하여 Eloquent 모델 관계를 구축하는 방법을 살펴보겠습니다. 먼저 애플리케이션에 App\Models\User 모델과 App\Models\Post 모델이 있다고 가정해 보겠습니다. 또한 User 모델이 PosthasMany 관계를 정의한다고 가정해 보겠습니다. 라라벨의 팩토리에서 제공하는 has 메소드를 사용하여 세 개의 게시물이 있는 사용자를 만들 수 있습니다. has 메소드는 팩토리 인스턴스를 입력받습니다.

use App\Models\Post;
use App\Models\User;

$user = User::factory()
            ->has(Post::factory()->count(3))
            ->create();

관례적으로 Post 모델을 has 메소드에 전달할 때 라라벨은 User 모델에 관계를 정의하는 posts 메소드가 있어야한다고 가정합니다. 필요한 경우 조작하려는 관계의 이름을 명시적으로 지정할 수 있습니다.

$user = User::factory()
            ->has(Post::factory()->count(3), 'posts')
            ->create();

물론 관련 모델에 대해 상태 조작을 수행할 수 있습니다. 또한 상태 변경에 상위 모델에 대한 액세스가 필요한 경우 클로저 기반 상태 변환을 전달할 수 있습니다.

$user = User::factory()
            ->has(
                Post::factory()
                        ->count(3)
                        ->state(function (array $attributes, User $user) {
                            return ['user_type' => $user->type];
                        })
            )
            ->create();

매직 메소드 사용

편의를 위해 라라벨의 매직 팩토리 관계 메소드를 사용하여 관계를 구축할 수 있습니다. 예를 들어, 다음 예는 관례를 사용하여 User 모델의 posts 관계 메소드를 통해 관련 모델을 생성해야 하는지 결정합니다.

$user = User::factory()
            ->hasPosts(3)
            ->create();

매직 메소드를 사용하여 팩토리 관계를 만들 때 관련 모델을 재정의 할 속성 배열을 전달할 수 있습니다.

$user = User::factory()
            ->hasPosts(3, [
                'published' => false,
            ])
            ->create();

상태 변경에 상위 모델에 대한 액세스가 필요한 경우 클로저 기반 상태 변환을 제공할 수 있습니다.

$user = User::factory()
            ->hasPosts(3, function (array $attributes, User $user) {
                return ['user_type' => $user->type];
            })
            ->create();

1:*(일대다) 역관계

이제 팩토리를 사용하여 "많은" 관계를 구축하는 방법을 살펴보았으므로 관계의 역을 살펴보겠습니다. for 메소드는 공장에서 생성된 모델이 속한 상위 모델을 정의하는 데 사용할 수 있습니다. 예를 들어 단일 사용자에게 속한 세 개의 App\Models\Post 모델 인스턴스를 만들 수 있습니다.

use App\Models\Post;
use App\Models\User;

$posts = Post::factory()
            ->count(3)
            ->for(User::factory()->state([
                'name' => 'Jessica Archer',
            ]))
            ->create();

생성 중인 모델과 연결되어야 하는 상위 모델 인스턴스가 이미 있는 경우 모델 인스턴스를 for 메서드에 전달할 수 있습니다.

$user = User::factory()->create();

$posts = Post::factory()
            ->count(3)
            ->for($user)
            ->create();

매직 메소드 사용

편의를 위해 "belong to" 관계를 정의하기 위해 라라벨의 매직 팩토리 관계 메소드를 사용할 수 있습니다. 예를 들어 다음 예제에서는 규칙을 사용하여 세 개의 게시물이 Post 모델의 user 관계에 속해있어야 한다고 지정합니다.

$posts = Post::factory()
            ->count(3)
            ->forUser([
                'name' => 'Jessica Archer',
            ])
            ->create();

*:*(다대다) 관계

1:*(일대다) 관계와 같이 has 메소드를 사용하여 *:*(다대다) 관계 만들 수 있습니다.

use App\Models\Role;
use App\Models\User;

$user = User::factory()
            ->has(Role::factory()->count(3))
            ->create();

피벗 테이블 속성

모델을 연결하는 피벗 / 중간 테이블에 설정해야하는 속성을 정의해야하는 경우 hasAttached 메서드를 사용할 수 있습니다. 이 메서드는 피벗 테이블 속성 이름과 값의 배열을 두 번째 인수로받습니다.

use App\Models\Role;
use App\Models\User;

$user = User::factory()
            ->hasAttached(
                Role::factory()->count(3),
                ['active' => true]
            )
            ->create();

상태 변경에 관련 모델에 대한 액세스가 필요한 경우 클로저 기반 상태 변환을 제공 할 수 있습니다.

$user = User::factory()
            ->hasAttached(
                Role::factory()
                    ->count(3)
                    ->state(function (array $attributes, User $user) {
                        return ['name' => $user->name.' Role'];
                    }),
                ['active' => true]
            )
            ->create();

생성 중인 모델에 연결하려는 모델 인스턴스가 이미 있는 경우 모델 인스턴스를 hasAttached 메서드에 전달할 수 있습니다. 이 예제에서는 동일한 세 가지 역할이 세 사용자 모두에게 연결됩니다.

$roles = Role::factory()->count(3)->create();

$user = User::factory()
            ->count(3)
            ->hasAttached($roles, ['active' => true])
            ->create();

매직 메소드 사용

편의를 위해 라라벨의 매직 팩토리 관계 메소드를 사용하여 다대다 관계를 정의할 수 있습니다. 예를 들어, 다음 예제는 관례를 사용하여 관련 모델이 User 모델의 roles 관계 메소드를 통해 생성되어야 하는지 결정합니다.

$user = User::factory()
            ->hasRoles(1, [
                'name' => 'Editor'
            ])
            ->create();

다형성 관계

다형성 관계도 팩토리를 사용하여 만들 수 있습니다. 다형성 "morph many" 관계는 일반적인 "다수" 관계와 동일한 방식으로 생성됩니다. 예를 들어 App\Models\Post 모델에 App\Models\Comment 모델과 morphMany 관계가 있는 경우라면 다음과 같습니다.

use App\Models\Post;

$post = Post::factory()->hasComments(3)->create();

Morph To 관계

매직 메서드는 morphTo 관계를 만드는 데 사용할 수 없습니다. 대신 for 메서드를 직접 사용해야 하며 관계 이름을 명시적으로 제공해야 합니다. 예를 들어 Comment 모델에 morphTo 관계를 정의하는 commentable 메서드가 있다고 가정해 보겠습니다. 이 상황에서 for 메서드를 직접 사용하여 단일 게시물에 속하는 세 개의 댓글을 만들 수 있습니다.

$comments = Comment::factory()->count(3)->for(
    Post::factory(), 'commentable'
)->create();

다형성 다 대다 관계

다형성 "다대다"(morphToMany morphedByMany) 관계는 비다형성 "다대다" 관계처럼 생성될 수 있습니다.

use App\Models\Tag;
use App\Models\Video;

$videos = Video::factory()
            ->hasAttached(
                Tag::factory()->count(3),
                ['public' => true]
            )
            ->create();

물론 매직 has 메소드는 다형성 "다 대다" 관계를 생성하는 데에도 사용할 수 있습니다.

$videos = Video::factory()
            ->hasTags(3, ['public' => true])
            ->create();

팩토리 내 관계 정의

모델 팩토리 내에서 관계를 정의하려면 일반적으로 관계의 외래 키에 새 팩토리 인스턴스를 할당합니다. 이것은 일반적으로 belongsTomorphTo 관계와 같은 "역" 관계에 대해 수행됩니다. 예를 들어 게시물을 만들 때 새 사용자를 만들고 싶다면 다음과 같이 처리 할 수 있습니다.

use App\Models\User;

/**
 * Define the model's default state.
 *
 * @return array
 */
public function definition()
{
    return [
        'user_id' => User::factory(),
        'title' => $this->faker->title(),
        'content' => $this->faker->paragraph(),
    ];
}

관계의 열이 그것을 정의하는 팩토리에 의존하는 경우, 속성에 클로저를 할당할 수 있습니다. 클로저는 팩토리의 실행된 속성 배열을 받을 것입니다.

/**
 * Define the model's default state.
 *
 * @return array
 */
public function definition()
{
    return [
        'user_id' => User::factory(),
        'user_type' => function (array $attributes) {
            return User::find($attributes['user_id'])->type;
        },
        'title' => $this->faker->title(),
        'content' => $this->faker->paragraph(),
    ];
}

시더 실행하기

기능 테스트 중에 database seeders를 사용하여 데이터베이스를 채우려면 seed 메서드를 호출할 수 있습니다. 기본적으로 seed 메소드는 다른 모든 시드를 실행해야 하는 DatabaseSeeder를 실행합니다. 또는 seed 메서드에 특정 시드 클래스 이름을 전달합니다.

<?php

namespace Tests\Feature;

use Database\Seeders\OrderStatusSeeder;
use Database\Seeders\TransactionStatusSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Tests\TestCase;

class ExampleTest extends TestCase
{
    use RefreshDatabase;

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

        // Run a specific seeder...
        $this->seed(OrderStatusSeeder::class);

        // ...

        // Run an array of specific seeders...
        $this->seed([
            OrderStatusSeeder::class,
            TransactionStatusSeeder::class,
            // ...
        ]);
    }
}

또는 RefreshDatabase 특성-trait을 사용하는 각 테스트 전에 데이터베이스를 자동으로 시드하도록 라라벨에 지시할 수 있습니다. 기본 테스트 클래스에서 $seed 속성을 정의하여 이를 수행할 수 있습니다.

<?php

namespace Tests;

use Illuminate\Foundation\Testing\TestCase as BaseTestCase;

abstract class TestCase extends BaseTestCase
{
    use CreatesApplication;

    /**
     * Indicates whether the default seeder should run before each test.
     *
     * @var bool
     */
    protected $seed = true;
}

$seed 속성이 true면 테스트는 RefreshDatabase 특성-trait을 사용하는 각 테스트 전에 Database\Seeders\DatabaseSeeder 클래스를 실행합니다. 그러나 테스트 클래스에 seeder 속성을 정의하여 실행해야 하는 특정 시드를 지정할 수 있습니다.

use Database\Seeders\OrderStatusSeeder;

/**
 * Run a specific seeder before each test.
 *
 * @var string
 */
protected $seeder = OrderStatusSeeder::class;

사용 가능한 검증-Assertions

라라벨은 PHPUnit 기능 테스트를 위한 여러 데이터베이스 검증을 제공합니다. 아래에서는 이러한 각 검증에 대해 살펴볼 것입니다.

assertDatabaseCount

데이터베이스의 테이블에 지정된 수의 레코드가 포함되어 있는지 확인합니다.

$this->assertDatabaseCount('users', 5);

assertDatabaseHas

데이터베이스의 테이블에 주어진 키 값 쿼리 제약 조건과 일치하는 레코드가 포함되어 있는지 확인합니다.

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

assertDatabaseMissing

데이터베이스의 테이블에 주어진 키 값 쿼리 제약 조건과 일치하는 레코드가 포함되어 있지 않음을 확인합니다.

$this->assertDatabaseMissing('users', [
    'email' => '[email protected]',
]);

assertDeleted

assertDeleted는 주어진 Eloquent 모델이 데이터베이스에서 삭제되었는지 확인합니다.

use App\Models\User;

$user = User::find(1);

$user->delete();

$this->assertDeleted($user);

assertSoftDeleted 메소드는 주어진 Eloquent 모델이 "소프트 삭제"되었다고 확인하기 위해 사용될 수 있습니다.

$this->assertSoftDeleted($user);

assertModelExists

주어진 모델이 데이터베이스에 존재하는지 확인합니다.

use App\Models\User;

$user = User::factory()->create();

$this->assertModelExists($user);

assertModelMissing

주어진 모델이 데이터베이스에 존재하지 않는지 확인합니다.

use App\Models\User;

$user = User::factory()->create();

$user->delete();

$this->assertModelMissing($user);