19 분 소요

Model and Model Testing Iteration

이제부터 데이터베이스를 개선해서 다양한 유형의 제품과 해당 데이터를 저장하는 유연성을 향상 시킬 예정이다.

Design : Implementing Database Structure Changes

직접적으로 Brand 모델을 작업하는 경우에는 브랜드의 이름, 주소, 사업자 번호 등과 같은 정보를 기록할 수 있다. 이러한 정보는 브랜드 테이블에 확장될 수 있다.
하지만 현재는 단순히 브랜드 이름만 저장하는 상태이다.

브랜드 테이블은 현재 브랜드의 이름과 해당 브랜드가 사용 중인지 여부(활성 상태)만 저장하고 있다. 브랜드 레이블을 갖고 있기 때문에 제품 테이블에서 해당 브랜드 이름을 한 번만 입력하거나 삽입할 수 있다. 이로써 제품 테이블에서 모든 제품이 동일한 브랜드를 사용하는 경우에도 브랜드 이름이 중복되지 않게 된다. 예를 들어 1000개의 제품이 모두 동일한 브랜드를 사용한다면 브랜드 이름이 중복된 열이 없을 거다.

현재는 아래와 같은 개념으로만 테이블이 정리되어있다.

새롭게 변경한 테이블 관계다.

이제 제품과 관련된 속성을 저장할 수 있는 기능을 만들었다.
해당 속성은 제품과 관련된 모든 프로덕트 라인에 소유된다.
제품과 속성 값 사이를 연결 시켰는데 한 제품은 여러 속성 값을 가질 수 있고, 한 속성값은 여러 제품에 연결될 수 있다. 따라서 여기서는 다대다 연결을 사용한다.

또한 프로덕트 타입과 프로덕트 라인의 연결간의 일대다 연결은 너무 많은 속성들이 한 번에 보여진다면 상당히 많은 메모리가 할당 될 수 있다. 따라서 위와 같이 관계를 만들었다.

Build : Category table iteration

models.pu

  

from django.db import models

from mptt.models import MPTTModel, TreeForeignKey

from .fields import OrderField

from django.core.exceptions import ValidationError

  
  
class Category(MPTTModel):

    name = models.CharField(max_length=255, unique=True)

    slug = models.SlugField(max_length=255, unique=True)

    is_active = models.BooleanField(default=False)

    parent = TreeForeignKey("self", on_delete=models.PROTECT, null=True, blank=True)

    objects = IsActiveQueryset.as_manager()

  

    class MPTTmeta:

        order_insertion_by = ['name']

  

    def __str__(self):

        return self.name

Testing : Category Testing Iteration

factories.py

class CategoryFactory(factory.django.DjangoModelFactory):

    class Meta:

        model = Category

  

    name = factory.Sequence(lambda n: "Category_%d" % n)

    slug = factory.Sequence(lambda n: "test_slug_%d" % n)

현재 모델에서 여러가지로 값을 넣을 수 있는 slug를 추가 해주고 모델 테스트를 진행한다.

test_models.py

  

class TestCategoryModel:

    def test_str_method(self, category_factory):

        x = category_factory(name="test_cat")

        assert x.__str__() == "test_cat"

  

    def test_name_max_length(self, category_factory):

        name = "x" * 256

        obj = category_factory(name=name)

        with pytest.raises(ValidationError):

            obj.full_clean()

  

    def test_name_unique_field(self, category_factory):

        category_factory(name="test_cat")

        with pytest.raises(IntegrityError):

            category_factory(name="test_cat")

  

    def test_is_active_false_default(self, category_factory):

        obj = category_factory()

        assert obj.is_active is False

  

    def test_parent_category_on_delete_protect(self, category_factory):

        obj1 = category_factory()

        category_factory(parent=obj1)

        with pytest.raises(IntegrityError):

            obj1.delete()

  

    def test_parent_field_null(self, category_factory):

        obj1 = category_factory()

        assert obj1.parent is None

  

    def test_return_category_active_only_true(self, category_factory):

        category_factory(is_active=True)

        category_factory(is_active=False)

        qs = Category.objects.is_active().count()

        assert qs == 1
  1. test_str_method: category_factory를 이용하여 카테고리 인스턴스를 생성한 후, __str__() 메서드가 올바르게 동작하는지 테스트한다. __str__() 메서드는 카테고리의 이름을 반환해야 한다.

  2. test_name_max_length: category_factory를 이용하여 이름의 최대 길이를 초과하는 카테고리 인스턴스를 생성하고, 해당 인스턴스를 저장할 때 ValidationError 예외가 발생하는지 테스트한다. 이름의 최대 길이는 255로 설정되어 있는데, 이를 초과하면 유효성 검사에 실패한다.

    full_clean() 메서드는 필드에 설정된 제약조건(최대길이, 고유성 등)을 준수하는지 확인하는 작업을 검사한다. -> 유효하지 않을 경우 ValidationError 발생

  3. test_name_unique_field: category_factory를 이용하여 동일한 이름을 가진 두 개의 카테고리 인스턴스를 생성하고, 두 번째 인스턴스를 저장할 때 IntegrityError 예외가 발생하는지 테스트한다. 이름 필드는 유일해야 하므로, 중복된 이름을 가진 인스턴스를 저장할 수 없다.

  4. test_is_active_false_default: category_factory를 이용하여 카테고리 인스턴스를 생성한 후, is_active 필드가 기본값인 False로 설정되는지 테스트한다.

  5. test_parent_category_on_delete_protect: category_factory를 이용하여 두 개의 카테고리 인스턴스를 생성하고, 두 번째 인스턴스의 부모 필드를 첫 번째 인스턴스로 설정한 후, 첫 번째 인스턴스를 삭제할 때 IntegrityError 예외가 발생하는지 테스트한다. 부모 카테고리를 삭제할 때, 연관된 자식 카테고리가 삭제되지 않도록 on_delete=PROTECT 설정이 되어 있어야 한다.

  6. test_parent_field_null: category_factory를 이용하여 카테고리 인스턴스를 생성한 후, 해당 인스턴스의 부모 필드가 기본값인 None인지 테스트한다. 즉, 부모 카테고리가 없는 경우, 부모 필드가 None이 되어야 한다.

  7. test_return_category_active_only_true: category_factory를 이용하여 활성화된(is_active=True) 카테고리와 비활성화된(is_active=False) 카테고리를 각각 하나씩 생성한 후, Category.objects.is_active().count()를 통해 활성화된 카테고리만을 쿼리하여 개수를 확인하는 테스트한다. 이 테스트에서는 is_active()라는 사용자 정의 쿼리셋 메서드가 제대로 동작하는지 확인한다.

Testing : Product Table Iteration

models.py

class Product(models.Model):

    name = models.CharField(max_length=100)

    slug = models.SlugField(max_length=255)

    pid = models.CharField(max_length=10, unique=True)

    description = models.TextField(blank=True)

    is_digital = models.BooleanField(default=False)

    category = TreeForeignKey("Category", on_delete=models.PROTECT)


    is_active = models.BooleanField(default=False)

    objects = IsActiveQueryset.as_manager()

    created_at = models.DateTimeField(

        auto_now=True,

        editable=False,

    )

  

    def __str__(self):

        return self.name
  • pid, category, created_at 추가

factories.py

class ProductFactory(factory.django.DjangoModelFactory):

    class Meta:

        model = Product

  

    name = factory.Sequence(lambda n: "test_product_name_%d" % n)

    pid = factory.Sequence(lambda n: "0000_%d" % n)

    description = "test_description"

    is_digital = False

    category = factory.SubFactory(CategoryFactory)

    is_active = True

test_models.py

  

class TestProductModel:

    def test_str_method(self, product_factory):

        obj = product_factory(name="test_product")

        assert obj.__str__() == "test_product"

  

    def test_name_max_length(self, product_factory):

        name = "x" * 236

        obj = product_factory(name=name)

        with pytest.raises(ValidationError):

            obj.full_clean()

  

    def test_slug_max_length(self, product_factory):

        name = "x" * 256

        obj = product_factory(name=name)

        with pytest.raises(ValidationError):

            obj.full_clean()

  

    def test_pid_length(self, product_factory):

        pid = "x" * 11

        obj = product_factory(pid=pid)

        with pytest.raises(ValidationError):

            obj.full_clean()

  

    def test_is_digital_false_default(self, product_factory):

        obj = product_factory(is_digital=False)

        assert obj.is_digital is False

  

    def test_fk_category_on_delete_protect(self, category_factory, product_factory):

        obj1 = category_factory()

        product_factory(category=obj1)

        with pytest.raises(IntegrityError):

            obj1.delete()

  

    def test_return_product_active_only_true(self, product_factory):

        product_factory(is_active=True)

        product_factory(is_active=False)

        qs = Product.objects.is_active().count()

        assert qs == 1

  

    def test_return_product_active_only_false(self, product_factory):

        product_factory(is_active=True)

        product_factory(is_active=False)

        qs = Product.objects.count()

        assert qs == 2

Testing: Product Line Testing Iteration

models.py

  

class ProductLine(models.Model):

    price = models.DecimalField(decimal_places=2, max_digits=5)

    sku = models.CharField(max_length=10)

    stock_qty = models.IntegerField()

    product = models.ForeignKey(

        Product, on_delete=models.PROTECT, related_name="product_line"

        )

    is_active = models.BooleanField(default=False)

    order = OrderField(unique_for_field="product", blank=True)

    weight = models.FloatField()


    created_at = models.DateTimeField(

        auto_now_add=True,

        editable=False,

    )

  

    objects = IsActiveQueryset.as_manager()

  

    def clean(self):

        query_set = ProductLine.objects.filter(product=self.product)

        for object in query_set:

            if self.id != object.id and self.order == object.order:

                raise ValidationError("Duplicate value.")

  

    def save(self, *args, **kwargs):

        self.full_clean()

        return super(ProductLine, self).save(*args, **kwargs)

  

    def __str__(self):

        return str(self.sku)

OrderFieldunique_for_fieldproduct_line 필드를 가리킨다. 이 경우, ProductImage 모델에서 product_line에 속하는 이미지들의 order 값은 고유해야 한다. 즉, 같은 product_line에 속하는 이미지들은 중복된 order 값을 가지면 안 된다. 이렇게 하면 특정 상품 라인에 속하는 이미지들의 순서를 간편하게 지정하고, 중복을 방지할 수 있다.

as_manager()

.as_manager()는 Django 모델 클래스에서 사용자 정의 매니저를 생성하는 메서드다. 모델 클래스에서 이 메서드를 호출하면, 해당 클래스에 사용자 정의 매니저가 추가되며, 해당 매니저를 통해 모델 인스턴스를 쿼리하는 기능을 추가할 수 있다.

Django에서 기본적으로 제공하는 매니저인 objects를 사용하여 모델 인스턴스를 조회하고 관리할 수 있다. 그러나 때때로 추가적인 기능이 필요하거나 사용자 정의 쿼리셋을 사용하고 싶을 때가 있다. 이런 경우 .as_manager()를 사용하여 사용자 정의 매니저를 생성할 수 있다.

예를 들어, 다음과 같이 IsActiveQueryset이라는 사용자 정의 쿼리셋을 정의했다고 가정한다면:

from django.db import models

class IsActiveQueryset(models.QuerySet):
    def active(self):
        return self.filter(is_active=True)

class MyModel(models.Model):
    name = models.CharField(max_length=100)
    is_active = models.BooleanField(default=True)

    # 사용자 정의 매니저를 생성하여 IsActiveQueryset을 사용한다.
    objects = IsActiveQueryset.as_manager()

# 모델을 사용할 때, IsActiveQueryset에서 정의한 메서드인 active()를 사용할 수 있다.
active_objects = MyModel.objects.active()

위의 예시에서 IsActiveQuerysetMyModel에 사용자 정의 쿼리셋을 제공하는 클래스다. 이 사용자 정의 쿼리셋은 active()라는 메서드를 정의하여 is_active=True인 모델 인스턴스만 반환하는 기능을 추가했다.

그리고 objects = IsActiveQueryset.as_manager()를 통해 IsActiveQueryset을 사용하는 사용자 정의 매니저를 MyModel에 추가했다. 이렇게 하면 MyModel에서 .objects를 통해 IsActiveQueryset에 정의한 active() 메서드 등을 사용할 수 있게 된다.

test_models.py

  

class TestProductLineModel:

    def test_str_method(self, product_line_factory):

        obj = product_line_factory(sku="12345")

        assert obj.__str__() == "12345"

  

    def test_duplicate_order_values(self, product_line_factory, product_factory):

        obj = product_factory()

        product_line_factory(order=1, product=obj)

        with pytest.raises(ValidationError):

            product_line_factory(order=1, product=obj).clean()

  

    def test_field_decimal_places(self, product_line_factory):

        price = 1.001

        with pytest.raises(ValidationError):

            product_line_factory(price=price)

  

    def test_field_price_max_digits(self, product_line_factory):

        price = 1000.00

        with pytest.raises(ValidationError):

            product_line_factory(price=price)

  

    def test_field_sku_max_length(self, product_line_factory):

        sku = "x" * 11

        with pytest.raises(ValidationError):

            product_line_factory(sku=sku)

  

    def test_is_active_false_default(self, product_line_factory):

        obj = product_line_factory(is_active=False)

        assert obj.is_active is False

  

    def test_fk_product_on_delete_protect(self, product_factory, product_line_factory):

        obj1 = product_factory()

        product_line_factory(product=obj1)

        with pytest.raises(IntegrityError):

            obj1.delete()

  

    def test_return_product_active_only_true(self, product_line_factory):

        product_line_factory(is_active=True)

        product_line_factory(is_active=False)

        qs = ProductLine.objects.is_active().count()

        assert qs == 1

  

    def test_return_product_active_only_false(self, product_line_factory):

        product_line_factory(is_active=True)

        product_line_factory(is_active=False)

        qs = ProductLine.objects.count()

        assert qs == 2

Testing : Product Image Testing Iteration

models.py

  

class ProductImage(models.Model):

    url = models.ImageField(upload_to=None, default="test.jpg")

    alternative_text = models.CharField(max_length=100)

    product_line = models.ForeignKey(

        ProductLine, on_delete=models.CASCADE, related_name="product_image"

    )

    order = OrderField(unique_for_field="product_line", blank=True)

  

    def clean(self):

        query_set = ProductImage.objects.filter(product_line=self.product_line)

        for object in query_set:

            if self.id != object.id and self.order == object.order:

                raise ValidationError("Duplicate value.")

  

    def save(self, *args, **kwargs):

        self.full_clean()

        return super(ProductImage, self).save(*args, **kwargs)

  

    def __str__(self):

        return f"{self.product_line.sku}_img"

clean 메서드는 모델의 데이터가 유효한지 확인하고, 유효하지 않을 경우 ValidationError 예외를 발생시키는 역할을 한다. 특히, 해당 clean 메서드는 같은 product_line에 속하는 이미지들 중에서 order 값이 중복되는지를 확인한다.

예를 들어, 다음과 같은 상황을 가정해본다면:

  1. 상품 라인 “Shoes”에 이미지를 등록하려고 한다.
  2. 이미지들의 순서를 order 필드로 지정한다.

clean 메서드는 이미지가 저장될 때마다 호출되며, 이미지가 ProductImage.objects.filter(product_line=self.product_line)를 사용하여 같은 상품 라인에 속하는 이미지들을 가져온다. 이후 for 루프를 통해 이미지들을 하나씩 순회하면서 다음을 확인한다:

  1. 현재 이미지(self)와 다른 이미지의 order 값이 같은지 비교한다.
  2. 현재 이미지와 다른 이미지의 id 값이 다른지 비교한다. (즉, 다른 이미지가 현재 이미지가 아닌지 확인)

만약 다른 이미지와 order 값이 같고, 다른 이미지가 현재 이미지가 아니라면(즉, 같은 상품 라인에 속하는 다른 이미지 중에 같은 order 값을 가지는 이미지가 있다면), ValidationError 예외를 발생시킨다. 이 예외는 중복된 order 값을 가지는 이미지를 저장하지 못하도록 막는 역할을 한다.

예를 들어, 상품 라인 “Shoes”에 이미지를 등록하면서 order 값을 지정하는 경우:

  1. 첫 번째 이미지: order=1로 설정
  2. 두 번째 이미지: order=2로 설정
  3. 세 번째 이미지: order=1로 설정

이때 위 clean 메서드가 호출되면, 이미지들을 순회하면서 첫 번째 이미지와 세 번째 이미지의 order 값이 같고, 두 이미지가 서로 다르므로 ValidationError 예외가 발생하게 된다. 이로 인해 세 번째 이미지를 등록할 때 중복된 order 값을 가지는 이미지를 저장하지 못하게 된다.

test_models.py

  
class TestProductImageModel:

    def test_str_method(self, product_image_factory, product_line_factory):

        obj1 = product_line_factory(sku="12345")

        obj2 = product_image_factory(order=1, product_line=obj1)

        assert obj2.__str__() == "12345_img"

  

    def test_alternative_text_field_length(self, product_image_factory):

        alternative_text = "x" * 101

        with pytest.raises(ValidationError):

            product_image_factory(alternative_text=alternative_text)

  

    def test_duplicate_order_values(self, product_image_factory, product_line_factory):

        obj = product_line_factory()

        product_image_factory(order=1, product_line=obj)

        with pytest.raises(ValidationError):

            product_image_factory(order=1, product_line=obj).clean()

test_duplicate_order_valuesProductImage 모델의 clean 메서드가 제대로 동작하는지 확인한다. clean 메서드는 모델의 데이터가 유효한지 확인하고, 유효하지 않을 경우 ValidationError 예외를 발생시키는 역할을 한다. 이 테스트 코드에서는 ProductImage 모델에 대해 같은 상품 라인(product_line)에 속하는 이미지들의 order 필드가 중복되는지를 확인하는 것이 목표다.

  1. obj = product_line_factory(): product_line_factory를 사용하여 ProductLine 모델의 인스턴스를 생성한다.

  2. product_image_factory(order=1, product_line=obj): product_image_factory를 사용하여 ProductImage 모델의 인스턴스를 생성한다. 이 때 order=1product_line=obj를 인자로 넘겨주어서 order 필드의 값이 1인 이미지를 생성합니다. 즉, 첫 번째 이미지를 생성한다.

  3. product_image_factory(order=1, product_line=obj).clean(): 다시 product_image_factory를 사용하여 두 번째 이미지를 생성하고, clean 메서드를 직접 호출한다. 이렇게 하면 이미지 생성과 동시에 clean 메서드를 호출하여 데이터의 유효성을 검사한다.

  4. with pytest.raises(ValidationError): 이 부분은 테스트 코드가 ValidationError 예외를 발생시키는지 확인하는 부분이다. 즉, product_image_factory(order=1, product_line=obj).clean()를 실행할 때 ValidationError 예외가 발생하는지를 확인한다.

위 테스트 코드에서 주목해야 할 부분은 두 번째 이미지를 생성할 때 order 값을 1로 설정했다. 이미 첫 번째 이미지에서 order 값이 1로 설정되어 있기 때문에, 같은 상품 라인인 obj에 속하는 이미지들 중에서 order 값이 중복되는 상태가 된다.

clean 메서드는 이러한 중복된 상태를 방지하기 위해 이미지를 생성할 때마다 실행되며, 같은 상품 라인에 속하는 이미지들의 order 값이 중복되는 경우 ValidationError 예외를 발생시킨다. 따라서, 위 테스트 코드에서 두 번째 이미지를 생성할 때 order 값이 중복되기 때문에 ValidationError 예외가 발생하게 된다.

이렇게 테스트 코드는 ProductImage 모델의 clean 메서드가 중복된 order 값을 가진 이미지를 저장하지 못하도록 제대로 동작하는지를 확인한다.

Testing: Product Type Testing Iteration

models.py

class ProductType(models.Model):

    name = models.CharField(max_length=100)

    parent = models.ForeignKey(

        "self", on_delete=models.PROTECT, null=True, blank=True

    )

    attribute = models.ManyToManyField(

        Attribute,

        through="ProductTypeAttribute",

        related_name="product_type_attribute",

    )

  

    def __str__(self):

        return str(self.name)

parent 필드는 ForeignKey 로 자기 자신을 참조하는 필드다. 이렇게 자기 자신을 참조하는 필드를 가리켜 자기 참조라고 한다.

또한 자기참조는 다음가 같이 사용할 때 유용하다 예를 들어서

  1. “Electronics” 카테고리를 생성합니다.
  2. “Mobile Phones” 카테고리를 생성하고, 이 카테고리의 parent를 “Electronics”로 설정합니다.
  3. “Tablets” 카테고리를 생성하고, 이 카테고리의 parent를 “Electronics”로 설정합니다.
  4. “Smartphones” 카테고리를 생성하고, 이 카테고리의 parent를 “Mobile Phones”로 설정합니다.

이렇게 하면 “Electronics” 카테고리가 최상위 카테고리가 되고, “Mobile Phones”와 “Tablets”는 “Electronics”의 하위 카테고리가 되며, “Smartphones”는 “Mobile Phones”의 하위 카테고리가 된다. 이렇게 자기 참조를 사용하면 계층 구조를 간단하게 표현할 수 있다.

자기 참조는 데이터베이스에서 복잡한 관계를 표현하는 데 유용하며, 이를 통해 모델 간의 계층적인 구조를 쉽게 구성할 수 있습니다.

test_models.py

  

class TestProductTypeModel:

    def test_str_method(self, product_type_factory):

        obj = product_type_factory.create(name="test_type")

        assert obj.__str__() == "test_type"

  

    def test_name_field_max_length(self, product_type_factory):

        name = "x" * 101

        obj = product_type_factory(name=name)

        with pytest.raises(ValidationError):

            obj.full_clean()

Testing: Attribute testing Iteration

factories.py

class ProductTypeFactory(factory.django.DjangoModelFactory):

    class Meta:

        model = ProductType

  

    name = factory.Sequence(lambda n: "test_type_name_%d" % n)

  

    # M2M

    @factory.post_generation

    def attribute(self, create, extracted, **kwargs):

        if not create or not extracted:

            return

        self.attribute.add(*extracted)

class AttributeFactory(factory.django.DjangoModelFactory):

    class Meta:

        model = Attribute

  

    name = "attribute_name_test"

    description = "attr_description_test"
  1. ProductTypeFactory: 이 팩토리는 ProductType 모델의 인스턴스를 생성하는 데 사용된다. 이 팩토리는 name 속성을 람다 함수로 정의하여 일련 번호를 이용해 고유한 이름을 생성한다. attribute 속성은 ProductType 모델과 Attribute 모델 간의 다대다 관계를 나타낸다. 이 속성은 생성 후 후크로 정의되어 있으며, ProductType 인스턴스가 생성된 후에 호출된다.

  2. AttributeFactory: 이 팩토리는 Attribute 모델의 인스턴스를 생성하는 데 사용된다. namedescription 두 가지 속성이 있으며, 각각 특정 테스트 값으로 미리 지정되어 있다.

@factory.post_generation 데코레이터: @factory.post_generation 데코레이터는 ProductTypeFactory에서 attribute 속성에 대한 후생 생성 후크를 정의하기 위해 사용된다. 후생 생성 후크를 사용하면 초기 생성이 완료된 후에 생성된 객체에 대해 추가적인 작업을 수행할 수 있다.


생성후크?

생성 후크(Factory post-generation hook)는 factory_boy 라이브러리에서 사용되는 개념으로, 팩토리를 통해 모델 인스턴스가 생성된 후 추가적인 작업을 수행할 수 있도록 해주는 기능이다. 이를 이용하면 생성된 객체를 더욱 정교하게 조작하고 초기화할 수 있다.

팩토리를 사용하여 모델 인스턴스를 생성하는 과정은 다음과 같다.

  1. 인스턴스가 생성될 때 기본 속성들이 적용된다.
  2. 팩토리가 post_generation 데코레이터로 정의된 후생 생성 후크를 호출한다.
  3. 후크 함수는 팩토리가 생성한 인스턴스에 추가적인 작업을 수행한다.

후생 생성 후크를 사용하면 생성된 인스턴스를 조작하여 더욱 다양한 상태로 초기화할 수 있다. 이를 통해 생성된 모델 인스턴스들은 테스트 환경에서 다양한 상황을 재현하기에 유용하다.

factory_boy에서 후생 생성 후크를 정의하려면 다음과 같은 방식으로 작성한다.

import factory

class MyModelFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = MyModel

    # 기본 속성들을 정의합니다.
    attribute1 = "default_value1"
    attribute2 = "default_value2"

    # 후생 생성 후크를 정의합니다.
    @factory.post_generation
    def my_custom_hook(instance, create, extracted, **kwargs):
        # instance: 생성된 모델 인스턴스
        # create: True이면 데이터베이스에 저장되고 False이면 단순히 객체가 생성됨
        # extracted: 생성할 때 명시적으로 전달된 값 (e.g., MyModelFactory(attribute1="new_value"))
        
        # 후크 함수 내에서 원하는 추가 작업을 수행합니다.
        if create and extracted:
            instance.attribute1 = extracted
            instance.save()

위의 예제에서 my_custom_hook 함수는 후생 생성 후크로 정의되었다. 이 함수는 MyModelFactory로 모델 인스턴스를 생성할 때 호출된다. create가 True이고 extracted가 전달된 경우, 인스턴스의 attribute1 속성을 extracted로 업데이트하고 저장한다. 이렇게 하면 MyModelFactory로 생성된 모든 인스턴스들에 대해 후크가 적용되며, 원하는 방식으로 인스턴스를 수정하거나 초기화할 수 있다.

후생 생성 후크는 데이터베이스에 저장되는 모델에 대해 특히 유용하다. 테스트 데이터를 생성하고 테스트 시나리오를 재현하는 데 도움이 된다.


처음으로 돌아가자면 attribute 후생 생성 후크는 ProductTypeAttribute 간의 다대다 관계를 처리하기 위해 정의되었다. 이 후크는 다음과 같은 매개변수를 받는다:

  • self: 생성 중인 ProductType 인스턴스 자체다.
  • create: 인스턴스가 생성되는지(True) 또는 단순히 빌드되는지(False)를 나타내는 true/false 플래그다.
  • extracted: ProductType 인스턴스를 생성할 때 attribute 속성에 전달된 값이다. 이 경우 Attribute 인스턴스들의 쿼리셋 또는 리스트가 된다.

후생 생성 후크는 create가 True이고 extracted가 비어 있지 않은지를 확인한다. 두 가지 조건이 모두 충족되면, 생성 중인 ProductType 인스턴스의 attribute 다대다 필드에 관련된 Attribute 인스턴스들을 추가한다.

요약하면, ProductTypeFactoryProductType 인스턴스를 생성할 때 attribute 속성에 Attribute 인스턴스들의 리스트를 전달하면 후생 생성 후크가 이들을 자동으로 매칭하여 다대다 관계를 설정한다.

Testing: Attribute Value Testing Iteration

models.py

class Product(models.Model):

    name = models.CharField(max_length=100)

    slug = models.SlugField(max_length=255)

    pid = models.CharField(max_length=10, unique=True)

    description = models.TextField(blank=True)

    is_digital = models.BooleanField(default=False)

    category = TreeForeignKey("Category", on_delete=models.PROTECT)

    product_type = models.ForeignKey(

        "ProductType", on_delete=models.PROTECT, related_name="product_type"

    )

    is_active = models.BooleanField(default=False)

    created_at = models.DateTimeField(

        auto_now_add=True,

        editable=False,

    )

    attribute_value = models.ManyToManyField(

        "AttributeValue",

        through="ProductAttributeValue",

        related_name="product_attr_value",

    )

    objects = IsActiveQueryset.as_manager()

  

    def __str__(self):

        return self.name


  
  

class ProductLineAttributeValue(models.Model):

    attribute_value = models.ForeignKey(

        AttributeValue,

        on_delete=models.CASCADE,

        related_name="product_attribute_value_av",

    )

    product_line = models.ForeignKey(

        "ProductLine",

        on_delete=models.CASCADE,

        related_name="product_attribute_value_pl",

    )

  

    class Meta:

        unique_together = ("attribute_value", "product_line")

  

    def clean(self):

        qs = (

            ProductLineAttributeValue.objects.filter(

                attribute_value=self.attribute_value

            )

            .filter(product_line=self.product_line)

            .exists()

        )

  

        if not qs:

            iqs = Attribute.objects.filter(

                attribute_value__product_line_attribute_value=self.product_line

            ).values_list("pk", flat=True)

  

            if self.attribute_value.attribute.id in list(iqs):

                raise ValidationError("Duplicate attribute exists")

  

    def save(self, *args, **kwargs):

        self.full_clean()

        return super(ProductLineAttributeValue, self).save(*args, **kwargs)

  

class ProductLine(models.Model):

    price = models.DecimalField(decimal_places=2, max_digits=5)

    sku = models.CharField(max_length=10)

    stock_qty = models.IntegerField()

    product = models.ForeignKey(

        Product, on_delete=models.PROTECT, related_name="product_line"

    )

    is_active = models.BooleanField(default=False)

    order = OrderField(unique_for_field="product", blank=True)

    weight = models.FloatField()

    attribute_value = models.ManyToManyField(

        AttributeValue,

        through="ProductLineAttributeValue",

        related_name="product_line_attribute_value",

    )

    product_type = models.ForeignKey(

        "ProductType", on_delete=models.PROTECT, related_name="product_line_type"

    )

    created_at = models.DateTimeField(

        auto_now_add=True,

        editable=False,

    )

    objects = IsActiveQueryset.as_manager()

  

    def clean(self):

        qs = ProductLine.objects.filter(product=self.product)

        for obj in qs:

            if self.id != obj.id and self.order == obj.order:

                raise ValidationError("Duplicate value.")

  

    def save(self, *args, **kwargs):

        self.full_clean()

        return super(ProductLine, self).save(*args, **kwargs)

  

    def __str__(self):

        return str(self.sku)

factories.py

class ProductFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Product

    name = factory.Sequence(lambda n: "test_product_name_%d" % n)
    pid = factory.Sequence(lambda n: "0000_%d" % n)
    description = "test_description"
    is_digital = False
    category = factory.SubFactory(CategoryFactory)
    is_active = True
    product_type = factory.SubFactory(ProductTypeFactory)

    @factory.post_generation
    def attribute_value(self, create, extracted, **kwargs):
        if not create or not extracted:
            return
        self.attribute_value.add(*extracted)

class ProductLineFactory(factory.django.DjangoModelFactory):

    class Meta:

        model = ProductLine

  

    price = 10.00

    sku = "0123456789"

    stock_qty = 1

    product = factory.SubFactory(ProductFactory)

    is_active = True

    weight = 100

    product_type = factory.SubFactory(ProductTypeFactory)

  

    @factory.post_generation

    def attribute_value(self, create, extracted, **kwargs):

        if not create or not extracted:

            return

        self.attribute_value.add(*extracted)


class AttributeValueFactory(factory.django.DjangoModelFactory):

    class Meta:

        model = AttributeValue

  

    attribute_value = "attr_test"

    attribute = factory.SubFactory(AttributeFactory)

  
  

class ProductLineAttributeValueFactory(factory.django.DjangoModelFactory):

    class Meta:

        model = ProductLineAttributeValue

  

    attribute_value = factory.SubFactory(AttributeValueFactory)

    product_line = factory.SubFactory(ProductLineFactory)

test_models.py

  
  

class TestProductLineModel:

    def test_duplicate_attribute_inserts(

        self,

        product_line_factory,

        attribute_factory,

        attribute_value_factory,

        product_line_attribute_value_factory,

    ):

        obj1 = attribute_factory(name="shoe-color")

        obj2 = attribute_value_factory(attribute_value="red", attribute=obj1)

        obj3 = attribute_value_factory(attribute_value="blue", attribute=obj1)

        obj4 = product_line_factory()

        product_line_attribute_value_factory(attribute_value=obj2, product_line=obj4)

        with pytest.raises(ValidationError):

            product_line_attribute_value_factory(

                attribute_value=obj3, product_line=obj4

            )


class TestAttributeValueModel:

    def test_str_method(self, attribute_value_factory, attribute_factory):

        obj_a = attribute_factory(name="test_attribute")

        obj_b = attribute_value_factory(attribute_value="test_value", attribute=obj_a)

        assert obj_b.__str__() == "test_attribute-test_value"

  

    def test_value_field_max_length(self, attribute_value_factory):

        attribute_value = "x" * 101

        obj = attribute_value_factory(attribute_value=attribute_value)

        with pytest.raises(ValidationError):

            obj.full_clean()

다대다 관계 변경 부분을 연결시키고 중복검사가 제대로 이루어지는지 확인한다.

댓글남기기