Using TestCase & setUpTestData() with M2M Fields

I’m trying to use TestCase and setUpTestData() to create a test of a model. There are several problems in the below code (marked with "TODO"s), but right now I’m just focusing on the users_following_cause=cls.user part where I was trying to set up a M2M field for the test data. I’ve tried a few iterations based on online answers but all throw errors and I’m still struggling. The full error message when I use the code shown in this post is:

============================================================================ ERRORS ============================================================================
______________________________________________ ERROR at setup of CauseTests.test_cause_createview_anonymous_user _______________________________________________

cls = <class 'causes.tests.CauseTests'>

    @classmethod
    def setUpTestData(cls):
        cls.user = get_user_model().objects.create_user(
            username="testuser1",
            email="test@email.com",
            password="testpass123",
        )
    
>       cls.cause = Cause.objects.create(
            #date_added is auto_now so it fills this field in automatically
            cause_name="Cause Name",
            created_by=cls.user,
            users_following_cause=cls.user, # TODO: figure out how to do the testing for now M2M fields...
        )

causes/tests.py:18: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.venv/lib/python3.10/site-packages/django/db/models/manager.py:85: in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
.venv/lib/python3.10/site-packages/django/db/models/query.py:512: in create
    obj = self.model(**kwargs)
.venv/lib/python3.10/site-packages/django/db/models/base.py:554: in __init__
    _setattr(self, prop, kwargs[prop])
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <django.db.models.fields.related_descriptors.ManyToManyDescriptor object at 0x112239150>, instance = <Cause: Cause Name>, value = <CustomUser: testuser1>

    def __set__(self, instance, value):
>       raise TypeError(
            "Direct assignment to the %s is prohibited. Use %s.set() instead."
            % self._get_set_deprecation_msg_params(),
        )
E       TypeError: Direct assignment to the forward side of a many-to-many set is prohibited. Use users_following_cause.set() instead.

.venv/lib/python3.10/site-packages/django/db/models/fields/related_descriptors.py:595: TypeError
=================================================================== short test summary info ====================================================================
ERROR causes/tests.py::CauseTests::test_cause_createview_anonymous_user - TypeError: Direct assignment to the forward side of a many-to-many set is prohibited. Use users_following_cause.set() instead.
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
======================================================================= 1 error in 1.26s =======================================================================

Other things I’ve tried
The error message clearly says TypeError: Direct assignment to the forward side of a many-to-many set is prohibited. Use users_following_cause.set() instead. but I’m just struggling to understand how to do this. Among many other things, I’ve tried:

  • users_following_cause=users_following_cause.set(cls.user) which results in this error: NameError: name 'users_following_cause' is not defined
  • users_following_cause=cls.cause.users_following_cause.set(cls.user) which results in this error: AttributeError: type object 'CauseTests' has no attribute 'cause'

My Question
Can you please help me understand how to add a M2M field to a test?

tests.py

class CauseTests(TestCase):
    @classmethod
    def setUpTestData(cls):
        cls.user = get_user_model().objects.create_user(
            username="testuser1",
            email="test@email.com",
            password="testpass123",
        )

        cls.cause = Cause.objects.create(
            #date_added is auto_now so it fills this field in automatically 
            cause_name="Cause Name",
            created_by=cls.user,
            users_following_cause=cls.user, # TODO: figure out how to do the testing for now M2M fields
        )

    def test_cause_model(self):
        #self.assertEqual(self.cause.date_added, ) TODO: figure out how to do the testing for "now" when the "now" of the creation and the "now" of the test is different
        self.assertEqual(self.cause.cause_name, "Cause Name")
        self.assertEqual(self.cause.created_by.username, "testuser1")
        #self.assertEqual(self.cause.users_following_cause.email, "test@email.com") # TODO: figure out how to do the testing for now M2M fields
        self.assertEqual(str(self.cause), "Cause Name")
        #self.assertEqual(self.cause.get_absolute_url(), "/causes/1/")  # TODO: figure out how to do this when pk is UUID?

models.py

CustomUser = get_user_model()

class Cause(models.Model):
    id = models.UUIDField( 
        primary_key=True,
        default=uuid.uuid4,
        editable=False)
    date_added = models.DateTimeField(auto_now_add=True)
    created_by = models.ForeignKey(CustomUser, on_delete=models.CASCADE)
    cause_name = models.CharField(max_length=100)
    users_following_cause = models.ManyToManyField(CustomUser, related_name="causes_user_follows", blank=True)

    def __str__(self):
        return self.cause_name
    
    def get_absolute_url(self):
        return reverse("cause_detail", args=[str(self.id)])

Thank you in advance.

This is covered in the docs at Many-to-many relationships | Django documentation | Django

Essentially, the situation is that you don’t assign directly to the ManyToMany field, you use the add method to add related objects.

So for example, instead of:

in the create method, you would create the Cause object first, then assign the relationship after the object has been created as:
cls.cause.users_following_cause.add(cls.user)

Side note: This is not specifically related to a test, this is the general case for relating the objects involved in any many-to-many relationship.

Always be aware of what a ManyToMany relationship is within the database. It’s a third table with a ForeignKey to each of the two tables being related. This means that both objects involved in the relationship must be in their respective tables before you can do the add.

1 Like

ahh, I see. The objects have to be created and then the fields set. Thanks, Ken!

A couple of related questions (updated code shown below questions):

  1. Why is the self.assertEqual(self.cause.users_following_cause.username, "testuser1") throwing an AttributeError for username when (what I thought was) the same thing works fine for this part: self.assertEqual(self.cause.created_by.username, "testuser1")? Do I have to do something differently because it was a M2M field?
  2. In the original error, the message says to Use users_following_cause.set() instead. — I was looking up the difference between add() and set() and trying to understand when to use which one and why. The particulars are still confusing to me, but it seems like the gist is that you would use add() when you want to add a single item into a collection and you would use set() when you’re trying to add multiple items in a list into an existing collection. Is that in the ballpark for an accurate rule of thumb?

tests.py

class CauseTests(TestCase):
    @classmethod
    def setUpTestData(cls):
        cls.user = get_user_model().objects.create_user(
            username="testuser1",
            email="test@email.com",
            password="testpass123",
        )

        cls.cause = Cause.objects.create(
            #date_added is auto_now so it fills this field in automatically 
            cause_name="Cause Name",
            created_by=cls.user,
        )
        cls.cause.users_following_cause.add(cls.user) 

    # MODEL TESTS #################################################################
    def test_cause_model(self):
        #self.assertEqual(self.cause.date_added, ) TODO: figure out how to do the testing for "now" when the "now" of the creation and the "now" of the test is different
        self.assertEqual(self.cause.cause_name, "Cause Name")
        self.assertEqual(self.cause.created_by.username, "testuser1")
        self.assertEqual(self.cause.users_following_cause.count(), 1)
        self.assertEqual(self.cause.users_following_cause.username, "testuser1") # throws an AttributeError: 'ManyRelatedManager' object has no attribute 'username'
        self.assertEqual(str(self.cause), "Cause Name")
        #self.assertEqual(self.cause.get_absolute_url(), "/causes/1/")  # TODO: figure out how to do this when pk is UUID?

causes/models.py [unchanged from first post]

accounts/models.py

class CustomUser(AbstractUser, PermissionsMixin):
    pass

For clarity and precision - you can set fields while you’re creating the object. However, defining a relation between two models in an M2M relationship does not set or affect any field in either model. What’s happening is that a row is being inserted into a different table (the through table).

This is covered at Related objects reference | Django documentation | Django, and again, the basic cause is that a ManyToMany relationship is not a field in the model. It’s a reference from a different model, and as such, is treated the same way as a reverse foreign key reference.

Since it is a Many-to-Many relationship, there is no one user/username related to the Cause. You need to work with users_following_cause as a queryset. (e.g., self.cause.users_following_cause.all() references all related User to that Cause.)

If you’re looking to check to see if there is at least one User related to self.cause having username == "testuser1" then you could check self.cause.users_following_cause.filter(username="testuser1").exists()

Not quite. The add does add the object(s) to the relationship, but set replaces the current related objects with the provided set. Both functions accept multiple objects as parameters.

1 Like

Thank you so much for elaborating & sending the links to read more; this has been super helpful!