Testing models with ManyToManyField (through a custom intermediary table/model)

In short.
Trying 2 ways to create an object with m2m field in test setup. Both fail.
1-
a = Model1.objects.create()
b = Model2.objects.create(a=a)
Fails with “TypeError: Direct assignment to the forward side of a many-to-many set is prohibited. Use a.set() instead.”
2-
b = Model2.objects.create()
b.save()
b.a.add(a) Or b.a.set([a])
Fails with “django.db.utils.IntegrityError: NOT NULL constraint failed: myapp_b.a_id”

What am I missing?

Full code
Models

class Color(models.Model):
    name = models.CharField()

class Size(models.Model):
    name = models.CharField()

class Tshirt(models.Model):
    name = models.CharField()
    color = models.ManyToManyField(Color, through="TshirtColorSize", related_name="+")
    size = models.ManyToManyField(Size, through="TshirtColorSize", related_name="+")
    weight = models.IntegerField()
    material = models.TextField(blank=True)

class TshirtColorSize(models.Model):
    tshirt = models.ForeignKey(Tshirt, on_delete=models.CASCADE)
    color = models.ForeignKey(Color, on_delete=models.CASCADE)
    size = models.ForeignKey(Size, on_delete=models.CASCADE)
    photo = models.ImageField(blank=True, null=True)

Test
Fails with error N1

class TestHomeTshirtDetails(TestCase):
    def setUp(self):
        self.color = Color.objects.create(name="white")
        self.size = Size.objects.create(name="XS")
        self.tshirt = Tshirt.objects.create(
            name="CL",
            weight=1,
            material="100%",
            color=self.color,
            size=self.size,
        )
        self.tcs = TshirtColorSize.objects.create(
            tshirt=self.tshirt, color=self.color, size=self.size
        )

Fails with error N2

class TestHomeTshirtDetails(TestCase):
    def setUp(self):
        self.color = Color.objects.create(name="white", name_html="white")
        self.size = Size.objects.create(name="XS")
        self.tshirt = Tshirt.objects.create(
            name="CL",
            weight=1,
            material="100%",
        )
        self.tshirt.save()
        self.tshirt.color.add(self.color)
        self.tshirt.size.add(self.size)
        self.tcs = TshirtColorSize.objects.create()
        self.tcs.save()
        self.tcs.tshirt.add(self.tshirt)
        self.tcs.color.add(self.color)
        self.tcs.size.add(self.size)

    def test_tshirt_details_view(self):
        url = f"/calc/tshirt-details/CL/white/"
        response = self.client.get(url)
        self.assertEqual(200, response.status_code)

Error detail on the N2:

  • “django.db.utils.IntegrityError: NOT NULL constraint failed: home_tshirtcolorsize.size_id”
  • ‘INSERT INTO “home_tshirtcolorsize” (“tshirt_id”, “color_id”, “size_id”, “photo”) VALUES (?, ?, ?, ?) RETURNING “home_tshirtcolorsize”.“id”’
    params = (1, 1, None, ‘’)
    It looks like it only misses “size_id”, while “tshirt_id”, “color_id” are somehow ok: (1, 1, None, ‘’)

Test

 def test_size(self):
     self.assertEqual(self.size.id, 1)

passes.

Welcome @kkvero !

Yes, this isn’t going to work because you’re not providing any values for the other non-null fields in the through model when trying to do this.

I will point out that your models aren’t properly normalized - this structure has a number of potential errors and ambiguities.

While I am not specifically familiar with your particular requirements, I can guess from the names of the models and fields that Tshirt should have a many-to-many relationship with TshirtColorSize instead of the two individual m2m fields, and there should not be a tshirt field in TshirtColorSize.

1 Like

Thank you for looking at this!
So the flaw is in the design. Following your remarks I decided to go with:

class Color(models.Model):
    name = models.CharField()

class Size(models.Model):
    name = models.CharField()

class Tshirt(models.Model):
    name = models.CharField()
    weight = models.IntegerField()
    material = models.TextField(blank=True)

class TshirtColorSize(models.Model):
    tshirt = models.ForeignKey(Tshirt, on_delete=models.CASCADE)
    color = models.ForeignKey(Color, on_delete=models.CASCADE)
    size = models.ForeignKey(Size, on_delete=models.CASCADE)
    photo = models.ImageField(blank=True, null=True)

Now the tests setups work. With minor changes to the views the project as well. I hope it is also a better setup overall.
Another reason to do tests in parallel and not after.
Thank you for your help!