Posted 2025.01.22 09:30
By recoma
테스트코드를 작성하다 보면, 테스트를 시작하기 전에 데이터베이스에 테스트용 데이터를 집어넣는 과정을 거친다. 보통은 ORM으로 데이터들을 생성하기 때문에 가끔 이런 코드를 볼 수가 있다. (Django 기준)
def setUp(self) -> None:
self.user = User.objects.create(
readable_id="G1234512345",
customer_key="6d1fb2d032214636900ec93b94674d74",
name="userName",
email="email@gmail.com",
gender=1,
phone_number="01012341234",
birth_date="19990101",
user_sns_id="user_sns_id",
)
name
이나 email
같은 테스트 목적이 있는 데이터들을 제외한 나머지 데이터들은 그냥 임의로 적어 놓았음을 알 수 있다. 즉, 테스트에 필요하지 않아 랜덤으로 돌려도 되는 값들을 일일이 리터럴하게 집어넣은 것이다. 이런 식으로 진행하게 되면, 쓸떼없는 코드가 많아져 전체적으로 코드가 더럽게 보일게 뻔하다. 이를 해결하기 위한 라이브러리가 바로 factory_boy 이다.
factory_boy는 테스트시 필요한 값들을 랜덤하게 생성하는 역할을 한다. 그런데 그냥 아무 랜덤값을 주는 게 아니라, 테마에 따라 다른 형태의 값을 주는데, 예를 들어 임의의 이름이 필요하다 하면 Mario나 Luigi 같은 랜덤으로 돌린 이름을 주고, 주소를 요청하면 주소와 관련되게 임의값을 준다.
factory_boy 라는 라이브러리를 설치하는 것이기 때문에 방법은 간단하다.
$ pip install factory_boy
일반적인 사용법이라 함은 그냥 단순히 랜덤값 하나를 불러오는 것을 의미한다. 이 사용법은 매우 간단하다. 몇줄만 추가하면 되기 때문이다.
faker
를 import 하고 Faker
객체를 하나 만든다. 그 다음에 그 객체에서 내가 원하는 랜덤값 이름의 함수를 사용하면 된다. 예를 들어 내가 이름과 관련된 랜덤값을 원한다면, .name()
또는 .user_name()
을 사용하면 된다.
단,
name()
과user_name()
의 차이점이라면,name()
은 실제 사람의 이름으로 성과 이름이 분리되서 나오고user_name()
은 게임에서 나오는 게임유저 이름 정도만 생각하면 된다.
from faker import Faker
fake = Faker()
fake.name() # Shawn Miller
fake.user_name() # Miller
이름 말고도 다른 테마로도 가능하다.
fake.date_time() # datetime 타입의 랜덤 날짜
fake.sentence() # 영어 1문장: General apply international possible old fear.
그렇다면 아까 위에 제시했던 유저 생성 코드를 아래와 같이 변경할 수 있다.
from faker import Faker
def setUp(self) -> None:
fake = Faker()
self.user = User.objects.create(
readable_id=f"G{fake.random_digit(10)}",
customer_key=fake.uuid4(),
name=fake.user_name(),
email=fake.email(),
gender=fake.random_int(min=0, max=1),
phone_number=fake.phone_number(),
birth_date=str(fake.ramdon_digit(19900000, 20241231)),
user_sns_id=str(fake.random_digit(20)),
)
일단 상수값들이 사라졌다. 이제 임의 데이터 생성은 factory boy에서 알아서 해주기 때문이다. 하지만… 뭔가 코드가 더러운건 똑같아 보인다. 그리고 테스트 유저 생성을 이 한 테스트 파일 뿐만 아니라 여러 테스트 파일에도 사용이 될 텐데 이렇게 되면 또 똑같은 식의 코드를 작성해야 한다. 이때 factory boy는 factory.Factory를 사용함을 제안한다.
이전에 서술했던 fake.Faker
가 단순히 랜덤값을 생성시키는 역할을 한 다면, factory.Factory
는 랜덤값들을 생성함은 물론 테스트용 데이터베이스에 까지 친절하게 데이터를 저장해 주는 역할을 한다. 예를 들어 테스트용 User
데이터를 데이터베이스 저장한다고 할때, factory.Factory
를 상속한 UserFactory
를 구현한 다음에 UserFactory.create()
만 작성해 주면 알아서 데이터베이스를 저장해 주고, ORM 인스턴스를 내뱉는다. 사용법은 아래와 같다.
# factory.py
import factory
class ProfileFactory(factory.django.DjangoModelFactory)
class Meta:
model = Profile
... 이하 생략 ...
class UserFactory(factory.django.DjangoModelFactory):
class Meta:
model = User
readable_id = factory.LazyFunction(lambda: str(uuid4())[:10])
customer_key = factory.Faker("uuid4")
user_sns_id = factory.LazyFunction(lambda: str(uuid4())[:10])
name = factory.Faker("user_name")
email = factory.Faker("email")
gender = factory.LazyFunction(lambda: random.choice([0, 1]))
phone_number = factory.Faker("phone_number")
birth_date = "990101"
is_staff = factory.LazyAttribute(lambda e: e.is_staff)
profile = factory.SubFactory(ProfileFactory)
# test.py
class UserTestCase(APITestCaese):
def setUp(self) -> None:
fake = Faker()
self.profile = ProfileFactory.create()
self.user = UserFactory.create(
is_staff=False,
profile=self.profile,
)
위의 UserFactory
를 정의하는 코드에서 여러 가지의 factory
쪽 함수를 사용하고 있는데 설명하자면 아래와 같다.
name = factory.Faker("user_name")
factory.Faker
는 가장 기본적으로 쓰이는 함수로, 아까 설명했던 Faker
쪽 함수와 기능이 같다고 보면 된다. 다른 점이 있다면 Faker
에서의 함수 이름이, factory.Faker
에서는 문자열 파라미터로 쓰인다는 것이다.
readable_id = factory.LazyFunction(lambda: str(uuid4())[:10])
하지만 factory.Faker
로는 랜덤 생성이 불가능한 상황이 있다. 예를 들어 UUID를 랜덤으로 돌리는 것은 factory.Faker("uuid4")
로 해결이 가능하지만, 랜덤으로 돌린 다음, 앞의 10자를 제외한 나머지 문자열을 제거하는 것 까지는 할 수가 없다. 이때는 랜덤 함수를 직접 만들어야 하는데, factory.LazyFunction
이 이를 가능하게 해준다.
is_staff = factory.LazyAttribute(lambda e: e.is_staff)
# test.py
UserFactory.create(is_staff=False)
일부 요소들은 반드시 랜덤으로 돌려서는 안되고 개발자가 반드시 값을 지정해 줘야 할 있다. 이때 LazyAttribute
를 사용한다. 파라미터로 익명 함수(lambda
)를 사용함으로써 개발자가 굳이 별도의 로직을 구현할 필요 없이 입력 데이터를 가공해서 저장할 수도 있다.
birth_date = "990101"
랜덤을 돌리지 않고 그냥 고정시키고 싶다면 이렇게 네이티브 하게 작성하면 된다. 물론, create
에서 필요시 데이터를 변경할 수 도 있다.
class ProfileFactory(factory.django.DjangoModelFactory)
class Meta:
model = Profile
... 이하 생략 ...
class UserFactory(factory.django.DjangoModelFactory)
class Meta:
model = User
profile = factory.SubFactory(ProfileFactory)
데이터 저장 시, 관계되어 있는 자식 테이블의 데이터가 필요할 때가 있다. 이때 factory.SubFactory
를 사용한다.
필드명 | 설명 | 필드명 | 설명 |
---|---|---|---|
name | 실제 이름 | user_name | 이름(닉네임) |
이메일 주소 | phone_number | 전화번호 | |
address | 주소 | date | 날짜 |
date_time | 날짜 및 시간(datetime ) |
time | 시간 |
credit_card_number | 신용카드 번호 | credit_card_expire | 신용카드 만료일 |
text | 랜덤 텍스트 | sentence | 1문장 |
paragraph | 1문단(여러문장) | url | URL |
uuid4 | UUID | color_name | 색상이름 |
random_int(min: int, max: int ) |
랜덤 정수 | random_number(k ) |
K자리 랜덤 숫자 |
random_element(elements: list ) |
elements 중에 하나 랜덤 |