Model Bakery 사용 시 주의점

Django application 테스트 시 Django Model에 대한 Test fixture를 일일이 만들기 번거로울 때 주로 Model Bakery를 사용하여 model fixture를 만든다.

이 포스팅은 Model Bakery를 사용하면서 부딪힌 겪은 문제를 통해 알게된 것들을 정리한 것이다.

(포스팅에 나오는 코드는 여기에서 확인 가능하다.)


다음과 같은 단순한 형태의 Board, Reply Django model을 기반으로 진행하도록 하겠다.

1
2
3
4
5
6
7
8
9
10
from django.db import models as m


class Board(m.Model):
content = m.CharField(null=False, max_length=100)


class Reply(m.Model):
board = m.ForeignKey(Board, null=False, on_delete=m.CASCADE)
content = m.CharField(null=False, max_length=100)

1. make는 DB에 fixture data를 저장한다.

make를 통해 model fixture를 생성할 수 있다.
하지만 단순 fixture 생성이 아니라 DB에 저장한 후 이를 가져오는 것이다.

Model Bakery의 코드를 까보면 주석에 다음과 같이 persited instance를 생성한다고 적혀있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def make(
_model: Union[str, Type[ModelBase]],
_quantity: Optional[int] = None,
make_m2m: bool = False,
_save_kwargs: Optional[Dict] = None,
_refresh_after_create: bool = False,
_create_files: bool = False,
_using: str = "",
_bulk_create: bool = False,
**attrs: Any
):
"""Create a persisted instance from a given model its associated models.
It fill the fields with random values or you can specify which
fields you want to define its values by yourself.
"""

테스트 코드를 통해 make를 통해 만든 model fixture가 실제로 DB에 저장되는지 확인해보자.

1
2
3
4
5
6
7
8
9
10
11
12
from django.test import TestCase
from model_bakery import baker

from board.models import Board, Reply


class ModelBakeryTest(TestCase):
def test_make_saves_fixture_data_in_db(self):
reply = baker.make(Reply)

self.assertIsNotNone(Board.objects.get(pk=reply.board.pk))
self.assertIsNotNone(Reply.objects.get(pk=reply.pk))

위 코드는 통과한다.
위 테스트 코드를 통해 make를 통해 만든 model fixture와 해당 model과 foreign key로 묶인 model 또한 DB에 저장되는 것을 확인할 수 있다.

또힌, DB에 fixture 데이터를 저장을 하기 때문에 non-null value에 None을 할당하면 DB constraint로 인한 오류가 난다.
(아래 테스트 코드는 어떤 Exception이 발생하는지 확인하기 위해 try-except 문으로 처리하였다)

1
2
3
4
5
6
7
8
def test_make_when_non_null_value_is_None_then_throws_error(self):
try:
baker.make(Reply, board=None) # Exception should be raised

self.fail()
except Exception as exception:
print(exception.__class__) # <class 'django.db.utils.IntegrityError'>
print(exception) # NOT NULL constraint failed: board_reply.board_id

그러면 model fixture를 생성할 때 굳이 DB에 저장하고난 model이 아닌 단순 model 객체를 만들고 싶다면 어떨까?
그럴 때는 prepare를 사용하면 된다.
prepare는 주석에서 설명하는 것과 같이 persist 되지않은 instance를 생성한다.

1
2
3
4
5
6
7
8
9
10
11
def prepare(
_model: Union[str, Type[ModelBase]],
_quantity=None,
_save_related=False,
_using="",
**attrs
) -> Model:
"""Create but do not persist an instance from a given model.
It fill the fields with random values or you can specify which
fields you want to define its values by yourself.
"""

다음 테스트 코드는 prepare를 통해 생성된 model의 pk가 지정되지 않았는지 확인하는 테스트이다.

1
2
3
4
def test_prepare_does_not_save_fixture_data_in_db(self):
reply = baker.prepare(Reply)

self.assertIsNone(reply.pk)

추가로 _save_related=True로 option 값을 주면 foreign key로 묶인 model 객체는 persisted model을 만든다.

아래 테스트 코드에서는 Reply와 foreign key로 묶인 Board는 DB에 저장된 대신 Reply는 DB에 저장되지 않음을 확인할 수 있다.

1
2
3
4
5
def test_prepare_when_save_related_is_True_then_saves_related_model_fixture_in_db(self):
reply = baker.prepare(Reply, _save_related=True)

self.assertIsNotNone(Board.objects.get(pk=reply.board.pk))
self.assertIsNone(reply.pk)

2. Custom model Field는 generators.add를 사용한다.

먼저 다음과 같이 content field에 대해 새로운 ContentField class를 만들어서 Custom model Field를 간단히 세팅해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from django.db import models as m


class ContentField(m.CharField):
pass


class Board(m.Model):
content = ContentField(null=False, max_length=100)


class Reply(m.Model):
board = m.ForeignKey(Board, null=False, on_delete=m.CASCADE)
content = ContentField(null=False, max_length=100)

그리고 앞서 만들었던 테스트 코드를 다시 실행하면 아래와 같이 에러가 나며 테스트가 깨진다.

1
TypeError: <class 'board.models.ContentField'> is not supported by baker.

ContentFieldCharField를 상속받긴 하였지만 Model Bakery에서 지원하는 Field가 아니기 때문에 나는 오류이다.


참고로 Model Bakery에서 지원하는 Field 목록은 아래에서 확인 가능하다.
https://model-bakery.readthedocs.io/en/latest/how_bakery_behaves.html#currently-supported-fields


이 경우 generators.add를 통해 Custom model Field에 대한 fixture value generator를 설정해줘야한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
class ModelBakeryTest(TestCase):
def setUp(self):
def content_field_generator():
return "default content"

baker.generators.add('board.models.ContentField', content_field_generator)

def test_generators_add_returns_assigned_value(self):
board = baker.make(Board)
self.assertEquals(board.content, "default content")

board = baker.prepare(Board)
self.assertEquals(board.content, "default content")

참고:

Share