본문 바로가기

Backend/Ruby on Rails

Rails FactoryBot 알아보기

🤖 FactoryBot이란 무엇일까요?

팩토리봇을 사용하면 모델에 대한 더미 데이터를 만들 수 있습니다. 저는 주로 spec 테스트를 할 때 더미데이터를 넣기 위해 FactoryBot을 유용하게 사용하고 있습니다.

 


🎉 설치 방법

gem "factory_bot_rails"

 

이후에 RSpec을 사용한다면 아래와 같이 설정해줍니다.

RSpec.configure do |config|
  config.include FactoryBot::Syntax::Methods
end

👀 'Factory'에 대한 정의

각각의 Factory는 이름속성들로 구분됩니다. 여기서 '이름'은 객체를 추측하는데 사용됩니다. 아래 예시에서 이름은 user, 속성은 first_name, last_name, admin이 됩니다.

# 이건 유저 클래스구만!
FactoryBot.define do
  factory :user do
    first_name { "John" }
    last_name  { "Doe" }
    admin { false }
  end
end

 

 

만약 이름과 매핑되는 모델명이 다르다면 명시적으로 클래스를 지정할 수도 있습니다.

factory :admin, class: "User" # 이건 이름이 admin이지만, User클래스야 !!  (만약 명시적으로 지정하지 않았다면, 어드민 클래스라고 추측된다.)

 

상수로 지정할 수도 있습니다. 단, 즉시 로딩되므로 성능 문제가 발생할 수도 있으니 주의해서 사용해야 합니다.

factory :access_token, class: User

 

속성을 해시(Hash)로 정의하고 싶은 경우에는, 두 세트의 중괄호를 사용해야합니다.

factory :program do
  configuration { { auto_resolve: false, auto_define: true } }
end

각 클래스에 대해 하나의 팩토리를 가지도록 하는 것이 좋습니다. 또한 상속을 통해 다른 팩토리를 생성해서 각 클래스에 대한 공통 시나리오를 다룰 수 있습니다. 만약 같은 이름으로 여러 팩토리를 정의하려고 시도하면 오류가 발생하니 주의해서 사용해야합니다.


🔧 Factories 사용하기

빌드 전략 (Build strategies)

팩토리 봇은 빌드 전략을 몇 가지 제공합니다.

  • build
  • create
  • attributes_for
  • build_stubbed
# 저장되지 않은 유저를 반환한다.
user = build(:user)

# 유저를 저장하고, 저장된 유저를 반환한다.
user = create(:user)

# 유저를 빌드하는데 사용할 수 있는 해시를 반환한다.
attrs = attributes_for(:user)

# 정의된 모든 속성이 stubbed out된 객체를 반환한다.
stub = build_stubbed(:user)

# 위의 메서드 중 하나에 블록을 전달하면 반환 객체가 생성된다.
create(:user) do |user|
  user.posts.create(attributes_for(:post))
end

 

전략에 상관없이 해시를 전달하여 정의된 속성을 재정의할 수도 있습니다.

# 유저를 빌드하고, first_name을 재정의한다.
user = build(:user, first_name: "Joe")
user.first_name # => "Joe"

 

별칭 (Alias)

팩토리봇은 재사용하기 쉽도록 별칭을 제공합니다. 예를 들어, Post에서 User를 참조하는 속성이 있을 때 유용하게 쓰일 수 있습니다. 아래 코드를 보면 더 쉽게 이해할 수 있습니다.

factory :user, aliases: [:author, :commenter] do # 별칭으로 'author', 'commenter'을 지정한다.
  first_name { "John" }
  last_name { "Doe" }
  date_of_birth { 18.years.ago }
end

factory :post do
  # 'author'이라는 별칭을 사용했기 때문에 아래와 같이 쓸 필요가 없다.
  # association :author, factory: :user
  author
  title { "How to read a book effectively" }
  body { "There are five steps involved." }
end

factory :comment do
  # 'commenter'이라는 별칭을 사용했기 때문에 아래와 같이 쓸 필요가 없다.
  # association :commenter, factory: :user
  commenter
  body { "Great article!" }
end

 

종속 속성 (Dependent Attributes)

동적 속성을 통해 다른 속성의 값을 기반으로 값을 정의할 수 있습니다.

factory :user do
  first_name { "Joe" }
  last_name  { "Blow" }
  email { "#{first_name}.#{last_name}@example.com".downcase } # 이렇게 동적 속성을 정의할 수도 있다.
end

create(:user, last_name: "Doe").email # => "joe.doe@example.com"

 

임시 속성 (Transient Attributes)

임시 속성을 팩토리에 적용하여 로직을 실행하게 할 수도 있습니다.

factory :user do
  transient do
    rockstar { true }
  end

  name { "John Doe#{" - Rockstar" if rockstar}" }
end

create(:user).name #=> "John Doe - ROCKSTAR"
create(:user, rockstar: false).name #=> "John Doe"

 

콜백 (With callbacks)

콜백을 정의할 수도 있습니다.

factory :user do
  transient do
    upcased { false }
  end

  name { "John Doe" }

  after(:create) do |user, evaluator|
    user.name.upcase! if evaluator.upcased
  end
end

create(:user).name #=> "John Doe"
create(:user, upcased: true).name #=> "JOHN DOE"

 

가장 좋은 방법은 기본 팩토리에는 반드시 필요한 속성만 정의하고, 상속을 통해 구체적인 클래스를 정의하면 DRY 원칙을 지킬 수 있습니다.


🖇 Associations

암시적으로 정의하기 (Implicit definition)

팩토리 내에서 관계 연결이 가능합니다. 만약 팩토리 이름과 association 이름이 동일한 경우에는 생략이 가능합니다.

factory :post do
  # ...
  author
end

 

명시적으로 정의하기 (Explicit definition)

만약 팩토리 이름과 association 이름이 다르다면 명시적으로 정의가 가능합니다. 속성을 재정의할 때 특히 유용하게 사용됩니다.

factory :post do
  # ...
  association :author
end

 

인라인으로 정의하기 (Inline definition)

인라인으로 속성을 정의할 수도 있습니다. 하지만 attributes_for 전략을 사용할 때에는 값이 nil이 됩니다.

factory :post do
  # ...
  author { association :author }
end

이 외에도

  • 지정해서 정의하기 (Specifying the factory)
  • 속성 재정의하기 (Overriding attributes)
  • 연결 재정의하기 (Association overrides)

등이 있습니다.


1️⃣ Sequences

글로벌한 시퀀스 (Global sequences)

sequence 블록을 사용하면 시퀀스를 사용할 수 있습니다.

# 새로운 시퀀스 정의하기
FactoryBot.define do
  sequence :email do |n|
    "person#{n}@example.com"
  end
end

generate :email # => "person1@example.com"
generate :email # => "person2@example.com"

동적 속성을 사용해서 할 수도, 암시적인 속성으로 사용할 수도 있습니다.

# 동적 속성 사용하기
factory :invite do
  invitee { generate(:email) }
end

# 암시적 속성 사용하기
factory :user do
  email # `email { generate(:email) }`와 같다.
end

이 외에도

  • 인라인으로 사용하기 (Inline sequences)
  • 초기값 설정하기 (Initial value)
  • 블록 없이 사용하기 (Without a block)
  • 별칭으로 사용하기 (Aliases)
  • 다시 초기로 돌려서 사용하기 (Rewinding)

등이 있습니다.

⚠️ 시퀀스를 사용할 때에는 충돌이 나지 않게 조심해야한다.
factory :user do
  sequence(:email) { |n| "person#{n}@example.com" }
end

FactoryBot.create(:user, email: "person1@example.com")
FactoryBot.create(:user) # 이메일 속성이 unique하다면, 충돌이 일어난다.

🐳 Traits

Traits 정의하기

Traits를 사용하면 특성들을 그룹화하고 적용할 수 있습니다.

factory :user, aliases: [:author]

factory :story do
  title { "My awesome story" }
  author

  trait :published do
    published { true }
  end

  trait :unpublished do
    published { false }
  end

  trait :week_long_publishing do
    start_at { 1.week.ago }
    end_at { Time.now }
  end

  trait :month_long_publishing do
    start_at { 1.month.ago }
    end_at { Time.now }
  end

  factory :week_long_published_story,    traits: [:published, :week_long_publishing]
  factory :month_long_published_story,   traits: [:published, :month_long_publishing]
  factory :week_long_unpublished_story,  traits: [:unpublished, :week_long_publishing]
  factory :month_long_unpublished_story, traits: [:unpublished, :month_long_publishing]
end

 

암시적인 속성으로 적용하기(As implicit attributes)

factory :week_long_published_story_with_title, parent: :story do
  published
  week_long_publishing
  title { "Publishing that was started at #{start_at}" }
end

만약 trait 이름과 같은 팩토리나 시퀀스가 있는 경우에는 작동하지 않습니다.

 

속성 우선순위 (Attribute precedence)

최신 속성이 우선시됩니다.

factory :user do
  name { "Friendly User" }
  login { name }

  trait :active do
    name { "John Doe" }
    status { :active }
    login { "#{name} (active)" }
  end

  trait :inactive do
    name { "Jane Doe" }
    status { :inactive }
    login { "#{name} (inactive)" }
  end

  trait :admin do
    admin { true }
    login { "admin-#{name}" }
  end

  factory :active_admin,   traits: [:active, :admin]   # "admin-John Doe"로 로그인된다.
  factory :inactive_admin, traits: [:admin, :inactive] # "Jane Doe (inactive)"로 로그인된다.
end

 

자식 팩토리 (In child factories)

자식 팩토리의 경우, trait에 부여된 속성을 재정의 할 수 있습니다.

factory :user do
  name { "Friendly User" }
  login { name }

  trait :active do
    name { "John Doe" }
    status { :active }
    login { "#{name} (M)" }
  end

  factory :brandon do
    active
    name { "Brandon" }
  end
end

 

Traits 사용하기(Using traits)

factory :user do
  name { "Friendly User" }

  trait :active do
    name { "John Doe" }
    status { :active }
  end

  trait :admin do
    admin { true }
  end
end

# :active 상태를 가진 이름이 "Jon Snow"인 어드민 유저를 만든다.
create(:user, :admin, :active, name: "Jon Snow")

 

리스트를 만들 수도 있습니다.

factory :user do
  name { "Friendly User" }

  trait :admin do
    admin { true }
  end
end

# :active 상태를 가진 이름이 "Jon Snow"인 어드민 유저를 3개 만든다.
create_list(:user, 3, :admin, :active, name: "Jon Snow")

 

Enum Traits 사용하기 (Enum traits)

알아서 자동으로 traits을 정의해주기 때문에 수동으로 정의해줄 필요가 없습니다.

FactoryBot.define do
  factory :task
end

FactoryBot.build(:task, :queued)
FactoryBot.build(:task, :started)
FactoryBot.build(:task, :finished)

👏 참고

'Backend > Ruby on Rails' 카테고리의 다른 글

Rails Console 사용하기  (0) 2021.11.04