Search code examples
pythondjangoformsunit-testingdjango-tests

Unit test for Django Update form


I do not understand how to manage updates on forms and related unit tests and I would really appreciate some advises =)

I have a Company model, and related very simple CompanyForm:

class Company(models.Model):
    """
    Company informations
    - Detailed information for display purposes in the application
      but also used in documents built and sent by the application
    - Mail information to be able to send emails
    """
    company_name = models.CharField("nom", max_length=200)
    comp_slug = models.SlugField("slug")
    logo = models.ImageField(upload_to="img/", null=True, blank=True)
    use_groups = models.BooleanField("utilise les groupes", default=False)   # Company uses groups or not
    rules = [("MAJ", "Majorité"), ("PROP", "Proportionnelle")]   # Default management rule
    rule = models.CharField(
        "mode de scrutin", max_length=5, choices=rules, default="MAJ"
    )
    upd_rule = models.BooleanField("choisir la règle de répartition pour chaque événement", default=False)     # Event rule might change from one to another or always use default
    statut = models.CharField("forme juridique", max_length=50)
    siret = models.CharField("SIRET", max_length=50)
    street_num = models.IntegerField("N° de rue", null=True, blank=True)
    street_cplt = models.CharField("complément", max_length=50, null=True, blank=True)
    address1 = models.CharField("adresse", max_length=300)
    address2 = models.CharField(
        "complément d'adresse", max_length=300, null=True, blank=True
    )
    zip_code = models.IntegerField("code postal")
    city = models.CharField("ville", max_length=200)
    host = models.CharField("serveur mail", max_length=50, null=True, blank=True)
    port = models.IntegerField("port du serveur", null=True, blank=True)
    hname = models.EmailField("utilisateur", max_length=100, null=True, blank=True)
    fax = models.CharField("mot de passe", max_length=50, null=True, blank=True)
    use_tls = models.BooleanField("authentification requise", default=True, blank=True)

    class Meta:
        verbose_name = "Société"
        constraints = [
            models.UniqueConstraint(fields=["comp_slug"], name="unique_comp_slug")
        ]

    def __str__(self):
        return self.company_name

    @classmethod
    def get_company(cls, slug):
        """ Retreive company from its slug """
        return cls.objects.get(comp_slug=slug)


class CompanyForm(forms.ModelForm):
    company_name = forms.CharField(label="Société", disabled=True)

    class Meta:
        model = Company
        exclude = []

The view is very simple too:

@user_passes_test(lambda u: u.is_superuser or u.usercomp.is_admin)
def adm_options(request, comp_slug):
    '''
        Manage Company options
    '''
    company = Company.get_company(comp_slug)
    comp_form = CompanyForm(request.POST or None, instance=company)

    if request.method == "POST":
        if comp_form.is_valid():
            comp_form.save()

    return render(request, "polls/adm_options.html", locals())

This view works fine, I can update information (it's actually not used for creation, which is done thanks to the Django Admin panel).

Unfortunately, I'm not able to build unit tests that will ensure update works!
I tried 2 ways, but none of them worked. My first try was the following:

class TestOptions(TestCase):
    def setUp(self):
        self.company = create_dummy_company("Société de test")
        self.user_staff = create_dummy_user(self.company, "staff", admin=True)
        self.client.force_login(self.user_staff.user)

    def test_adm_options_update(self):
        # Load company options page
        url = reverse("polls:adm_options", args=[self.company.comp_slug])
        response = self.client.get(url)
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, "0123456789")
        self.assertEqual(self.company.siret, "0123456789")

        # Options update
        response = self.client.post(
            reverse("polls:adm_options", args=[self.company.comp_slug]),
            {"siret": "987654321"}
        )
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, "987654321")
        self.assertNotContains(response, "0123456789")
        self.assertEqual(self.company.siret, "987654321")

In this case, everything is OK but the latest assertion. It looks that the update has not been saved, which is actually not the case. I tried to Read the database just before, with the key stored in the context, but it remains the same.

I was looking for other information when I found this topic, so I tried another way to test, even if the approach surprised me a bit (I do not see how the view is actually tested).
Here is my second try (setUp() remains the same):

    def test_adm_options_update(self):
        # Load company options page
        url = reverse("polls:adm_options", args=[self.company.comp_slug])
        response = self.client.get(url)
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, "0123456789")         # this is the default value in tests for this field
        self.assertEqual(self.company.siret, "0123456789")

        # Options update
        self.company.siret = "987654321"
        comp_form = CompanyForm(instance=self.company)
        self.assertTrue(comp_form.is_valid())
        comp_form.save()
        company = Company.get_company(self.company.comp_slug)
        self.assertEqual(company.siret, "987654321")

In this case, the form is just empty!

I could consider my view works and go ahead, my problem is that I have a bug in another view and I would like to ensure I can build the test to find out the bug!

Many thanks in advance for your answers!

EDITS - Aug 30th
Following advices, I tried to use self.company.refresh_from_db() but it did not change the result.
A try was made to pass all fields in the self.client.post() but it fails as soon as a field is empty ('Cannot encode None as POST data' error message)
It also appeared that I created a 'dummy' company for test with empty mandatory fields... and it worked anyway. A matter of testing environment ? I changed this point but I wonder if the problem is not anywhere else...

EDITS - Sept 15th
Looking for someone available to provide me with new ideas, please =)

To ensure I understood the latest proposition, here is the complete code for the test:

def test_adm_options_update(self):
    # Load company options page
    url = reverse("polls:adm_options", args=[self.company.comp_slug])
    response = self.client.get(url)
    self.assertEqual(response.status_code, 200)
    self.assertContains(response, "0123456789")
    self.assertEqual(self.company.siret, "0123456789")

    # Apply changes
    company_data = copy.deepcopy(CompanyForm(instance=self.company).initial)
    company_data['siret'] = "987654321"
    response = self.client.post(
        reverse("polls:adm_options", args=[self.company.comp_slug]),
        company_data,
        )
    self.company.refresh_from_db()
    self.assertEqual(response.status_code, 200)
    self.assertContains(response, "987654321")
    self.assertNotContains(response, "0123456789")
    self.assertEqual(self.company.siret, "987654321")

Here is the function that creates the 'dummy' copany for tests:

def create_dummy_company(name):
    return Company.objects.create(
        company_name=name,
        comp_slug=slugify(name),
        logo=SimpleUploadedFile(name='logo.jpg', content=b'content', content_type='image/jpeg'),
        statut="SARL",
        siret="0123456789",
        address1="Rue des fauvettes",
        zip_code="99456",
        city='Somewhere',
        host="smtp.gmail.com",
        port=587,
        hname="[email protected]",
        fax="toto",
    )

Solution

  • In this case, you need to use refresh_from_db to "refresh" your object once the view and the form are done updating your object. This means that when you are currently asserting, you are using an "old snapshot" of self.company hence the failure on assertion, so you need to update it:

            # Options update
            response = self.client.post(
                reverse("polls:adm_options", args=[self.company.comp_slug]),
                {"siret": "987654321"}
            )
            ...
            self.company.refresh_from_db()
            self.assertEqual(self.company.siret, "987654321")
    

    EDIT:

    Figured out a way to make this work. Since the form requires that you put in all data, you can just pass the company instance to the same form, and access initial (which will serve as your request data).

    You can then modify it with the changes you want, in this case for siret and logo:

    from django.core.files.uploadedfile import SimpleUploadedFile
    
        def test(self):
            company_data = CompanyForm(instance=self.company).initial
            company_data['logo'] = SimpleUploadedFile(name='somefile', content=b'content', content_type='image/jpeg')
            company_data['siret'] = "987654321"
    
            response = self.client.post(
                reverse("polls:adm_options", args=[self.company.comp_slug]),
                company_data,
            )
    
            self.company.refresh_from_db()
            self.assertEqual(self.company.siret, "987654321")
    

    This works and passes on my end with the same exact model you have.