팩토리란
테스트나 데이터베이스 시딩에서는 데이터베이스에 레코드를 삽입할 필요가 있습니다.
수동으로 각 컬럼의 값을 지정하는 대신, Laravel의 모델 팩토리 를 사용하면 각 Eloquent 모델의 기본 속성 세트를 정의할 수 있습니다.
모든 새 Laravel 애플리케이션에는 database/factories/UserFactory.php가 포함되어 있습니다.
namespace Database\Factories ;
use Illuminate\Database\Eloquent\Factories\ Factory ;
use Illuminate\Support\Facades\ Hash ;
use Illuminate\Support\ Str ;
class UserFactory extends Factory
{
// 같은 비밀번호를 매번 재해시하지 않도록 캐시
protected static ? string $password ;
public function definition () : array
{
return [
'name' => fake () -> name (),
'email' => fake () -> unique () -> safeEmail (),
'email_verified_at' => now (),
'password' => static :: $password ??= Hash :: make ( 'password' ),
'remember_token' => Str :: random ( 10 ),
];
}
}
fake() 헬퍼를 통해 Faker 라이브러리를 이용할 수 있으며, 다양한 랜덤 데이터를 생성할 수 있습니다.
Faker의 로케일은 config/app.php의 faker_locale 옵션으로 변경할 수 있습니다.
팩토리 생성
make:factory Artisan 명령어로 팩토리를 작성합니다.
php artisan make:factory PostFactory
새로운 팩토리 클래스는 database/factories 디렉터리에 배치됩니다.
모델과 팩토리의 자동 검출
모델에 HasFactory 트레이트를 사용하면 Laravel이 자동으로 대응하는 팩토리를 찾습니다.
Database\Factories 네임스페이스에서 모델명에 Factory를 붙인 클래스명을 검색합니다.
명명 규칙이 맞지 않는 경우는 UseFactory 속성으로 명시적으로 지정할 수 있습니다.
use Illuminate\Database\Eloquent\Attributes\ UseFactory ;
use Database\Factories\Administration\ FlightFactory ;
#[ UseFactory ( FlightFactory :: class )]
class Flight extends Model
{
// ...
}
팩토리 정의
definition() 메서드
팩토리의 중심이 되는 definition() 메서드에서 기본 속성을 정의합니다.
namespace Database\Factories ;
use Illuminate\Database\Eloquent\Factories\ Factory ;
class PostFactory extends Factory
{
public function definition () : array
{
return [
'user_id' => \App\Models\ User :: factory (),
'title' => fake () -> sentence (),
'content' => fake () -> paragraphs ( 3 , true ),
'published_at' => fake () -> optional () -> dateTimeBetween ( '-1 year' , 'now' ),
];
}
}
Faker에서 자주 사용되는 메서드 목록:
메서드 생성되는 데이터 예시 fake()->name()John Doefake()->email()[email protected] fake()->sentence()랜덤한 문장 fake()->paragraph()랜덤한 단락 fake()->numberBetween(1, 100)1~100의 정수fake()->dateTime()랜덤한 일시 fake()->boolean()true / false
팩토리 스테이트(States)
스테이트 메서드를 사용하면 팩토리에 대해 개별 변경을 정의할 수 있습니다.
use Illuminate\Database\Eloquent\Factories\ Factory ;
class UserFactory extends Factory
{
public function definition () : array
{
return [
'name' => fake () -> name (),
'email' => fake () -> unique () -> safeEmail (),
'account_status' => 'active' ,
];
}
// 정지 중 사용자의 스테이트
public function suspended () : static
{
return $this -> state ( fn ( array $attributes ) => [
'account_status' => 'suspended' ,
]);
}
// 관리자 사용자의 스테이트
public function admin () : static
{
return $this -> state ( fn ( array $attributes ) => [
'is_admin' => true ,
]);
}
}
스테이트는 조합하여 사용할 수 있습니다.
$user = User :: factory () -> suspended () -> create ();
$admin = User :: factory () -> admin () -> create ();
소프트 삭제의 스테이트
소프트 삭제 대응 모델에는 빌트인의 trashed() 스테이트를 이용할 수 있습니다.
$user = User :: factory () -> trashed () -> create ();
팩토리 콜백(Callbacks)
afterMaking과 afterCreating으로 모델 생성 후의 추가 처리를 정의합니다.
namespace Database\Factories ;
use App\Models\ User ;
use Illuminate\Database\Eloquent\Factories\ Factory ;
class UserFactory extends Factory
{
public function configure () : static
{
return $this -> afterMaking ( function ( User $user ) {
// make() 후에 실행(DB에는 저장되어 있지 않음)
}) -> afterCreating ( function ( User $user ) {
// create() 후에 실행(DB에 저장 완료)
});
}
// ...
}
스테이트 메서드 내에서도 콜백을 등록할 수 있습니다.
public function withProfile () : static
{
return $this -> state ( fn ( array $attributes ) => [])
-> afterCreating ( function ( User $user ) {
$user -> profile () -> create ([
'bio' => fake () -> paragraph (),
]);
});
}
모델 생성
make() — DB 에 저장하지 않음
use App\Models\ User ;
// 1건 생성
$user = User :: factory () -> make ();
// 3건 생성(컬렉션으로 반환됨)
$users = User :: factory () -> count ( 3 ) -> make ();
create() — DB 에 저장
// 1건 생성해서 DB에 저장
$user = User :: factory () -> create ();
// 3건 생성해서 DB에 저장
$users = User :: factory () -> count ( 3 ) -> create ();
속성의 덮어쓰기
make()나 create()에 배열을 전달하면 특정 속성만 덮어쓸 수 있습니다.
$user = User :: factory () -> make ([
'name' => '山田太郎' ,
]);
$user = User :: factory () -> create ([
'name' => '鈴木花子' ,
'email' => '[email protected] ' ,
]);
스테이트를 사용한 인라인 덮어쓰기도 가능합니다.
$user = User :: factory () -> state ([
'name' => '田中一郎' ,
]) -> make ();
팩토리로 모델을 생성할 때 매스 어사인먼트 보호는 자동으로 무효화됩니다.
Sequence(시퀀스)
여러 모델을 생성할 때 속성을 번갈아 바꾸는 경우 Sequence를 사용합니다.
use App\Models\ User ;
use Illuminate\Database\Eloquent\Factories\ Sequence ;
$users = User :: factory ()
-> count ( 10 )
-> state ( new Sequence (
[ 'admin' => 'Y' ],
[ 'admin' => 'N' ],
))
-> create ();
// 5건은 admin='Y', 5건은 admin='N'으로 생성됨
sequence() 메서드를 사용하면 더 간결하게 쓸 수 있습니다.
$users = User :: factory ()
-> count ( 2 )
-> sequence (
[ 'name' => '최초의 사용자' ],
[ 'name' => '2번째 사용자' ],
)
-> create ();
클로저로 랜덤한 값을 동적으로 생성할 수도 있습니다.
use Illuminate\Database\Eloquent\Factories\ Sequence ;
$users = User :: factory ()
-> count ( 10 )
-> state ( new Sequence (
fn ( Sequence $sequence ) => [ 'name' => '사용자 ' . $sequence -> index ],
))
-> create ();
릴레이션십 팩토리
Has Many(1대다)
has() 메서드로 “1대다” 릴레이션을 가지는 모델을 생성합니다.
use App\Models\ Post ;
use App\Models\ User ;
// 3건의 Post를 가지는 User를 생성
$user = User :: factory ()
-> has ( Post :: factory () -> count ( 3 ))
-> create ();
매직 메서드를 사용하면 더 간결하게 쓸 수 있습니다(has + 릴레이션 이름의 복수형).
$user = User :: factory ()
-> hasPosts ( 3 )
-> create ();
// 속성을 덮어쓰기
$user = User :: factory ()
-> hasPosts ( 3 , [ 'published' => false ])
-> create ();
Belongs To(역 릴레이션)
for() 메서드로 “속하는” 부모 모델을 지정합니다.
use App\Models\ Post ;
use App\Models\ User ;
// 특정 사용자에 속하는 3건의 Post를 생성
$posts = Post :: factory ()
-> count ( 3 )
-> for ( User :: factory () -> state ([ 'name' => '田中花子' ]))
-> create ();
// 기존 모델을 부모로 사용
$user = User :: factory () -> create ();
$posts = Post :: factory () -> count ( 3 ) -> for ( $user ) -> create ();
매직 메서드 버전:
$posts = Post :: factory ()
-> count ( 3 )
-> forUser ([ 'name' => '田中花子' ])
-> create ();
Many to Many(다대다)
hasAttached() 메서드로 다대다 릴레이션을 다룹니다. 중간 테이블의 속성도 지정할 수 있습니다.
use App\Models\ Role ;
use App\Models\ User ;
$user = User :: factory ()
-> hasAttached (
Role :: factory () -> count ( 3 ),
[ 'active' => true ] // 중간 테이블의 속성
)
-> create ();
매직 메서드 버전:
$user = User :: factory ()
-> hasRoles ( 1 , [ 'name' => 'Editor' ])
-> create ();
폴리모픽 릴레이션
일반 “1대다”와 동일하게 has()나 매직 메서드를 사용할 수 있습니다.
use App\Models\ Post ;
$post = Post :: factory () -> hasComments ( 3 ) -> create ();
morphTo 릴레이션에는 매직 메서드를 사용할 수 없습니다. for()에 관계명을 명시합니다.
$comments = Comment :: factory () -> count ( 3 ) -> for (
Post :: factory (), 'commentable'
) -> create ();
팩토리 내에서의 릴레이션 정의
definition() 내에서 외래 키에 다른 팩토리를 할당하면, 모델 생성 시에 자동으로 부모 모델도 생성됩니다.
class PostFactory extends Factory
{
public function definition () : array
{
return [
'user_id' => \App\Models\ User :: factory (),
'title' => fake () -> sentence (),
'content' => fake () -> paragraph (),
];
}
}
recycle() — 기존 모델의 재사용
여러 릴레이션에서 같은 모델 인스턴스를 재사용하고 싶은 경우 recycle()을 사용합니다.
// 같은 Airline을 Ticket과 Flight 양쪽에서 사용
Ticket :: factory ()
-> recycle ( Airline :: factory () -> create ())
-> create ();
// 컬렉션에서 랜덤으로 선택
$airlines = Airline :: factory () -> count ( 3 ) -> create ();
Ticket :: factory () -> count ( 10 ) -> recycle ( $airlines ) -> create ();
시딩에서의 이용
DatabaseSeeder나 시더 클래스에서 팩토리를 호출합니다.
namespace Database\Seeders ;
use App\Models\ User ;
use Illuminate\Database\ Seeder ;
class DatabaseSeeder extends Seeder
{
public function run () : void
{
// 10건의 사용자를 생성하고, 각 사용자에게 3건의 투고를 갖게 함
User :: factory ()
-> count ( 10 )
-> hasPosts ( 3 )
-> create ();
}
}
시더 실행:
테스트에서의 이용
테스트에서는 RefreshDatabase 트레이트와 조합해서 팩토리를 사용합니다.
namespace Tests\Feature ;
use App\Models\ User ;
use App\Models\ Post ;
use Illuminate\Foundation\Testing\ RefreshDatabase ;
use Tests\ TestCase ;
class PostTest extends TestCase
{
use RefreshDatabase ;
public function test_user_can_view_their_posts () : void
{
$user = User :: factory () -> create ();
$posts = Post :: factory () -> count ( 3 ) -> for ( $user ) -> create ();
$response = $this -> actingAs ( $user )
-> get ( '/dashboard' );
$response -> assertOk ();
$response -> assertSee ( $posts -> first () -> title );
}
public function test_suspended_user_cannot_post () : void
{
$user = User :: factory () -> suspended () -> create ();
$response = $this -> actingAs ( $user )
-> post ( '/posts' , [ 'title' => '테스트' ]);
$response -> assertForbidden ();
}
}
RefreshDatabase는 테스트마다 데이터베이스를 리셋하기 때문에, 테스트 간에 데이터가 간섭하지 않습니다.
Pest로 테스트할 때도 팩토리의 API는 동일합니다.
uses(RefreshDatabase::class)를 사용해 데이터베이스를 리셋해 주십시오.
다음 단계
데이터베이스 시딩 시더와 팩토리를 조합해 초기 데이터를 투입하는 방법을 확인합니다.
테스트 입문 Laravel의 테스트 기능과 RefreshDatabase 트레이트의 사용법을 확인합니다.