Unit test for Django Update form

It’s really difficult for me to manage updates on forms and related unit tests and I would really appreciate some feedback!

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

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 as I want (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 (using my own get_company() method, or even refresh_from_db()), but it remains the same.

Here is my second try, built thanks to information found on the web, even if the approach surprised me a bit because I do not see how the view is actually tested (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, but I would like to understand what’s wrong. And, to be honest, 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!

What is the error in the first try?
Btw, 200 can be returned even if the operation failed.
Print the response of the post.
Check for response.context[‘name’].errors and such…

Another issue is that you post before you login.
It supposed to be like that?

As LiorA1 also indicated, it’s worth checking what the actual page being returned is, eg. by adding

  self.fail(response.content)

to your test right before the failure point, to dump the content of the response page - which hopefully should provide a hint.

Thanks for your answers.
The error is an AssertionError for the latest statement, the siret code has not changes (the form is actually not valid because of empty mandatory fields)

So the problem is not the test? (If I understand you)
You need to see what is the response…

BTW, I believe that after a POST you better use “redirect” and not “render”.

Hi LiorA1,
I’m sorry I was’nt clear enough.
The view works, I can properly update data.
Unit tests do not work, it seems the form is empty - actually not completely, some fields are populated, other are not anymore, I really do not understand. Finally, the database is not updated.
If I modify test to pass at least all required field, it works until there are no empty fields. If some optional fields are passed in the dictionary, will empty values, it does not work neither (the error message is different, it tells me he cannot consider None value as acceptable for a POST)

It sounds like the post is not supply all the required fields.

Please add the response of the post.

Sorry, but I’m not sure of what you expect with “add the response here”.

With no additional comments, I receive this test result:


======================================================================
FAIL: test_adm_options_update (polls.tests_admin.TestOptions)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "D:\Mes documents\Informatique\Developpement\Votes AG\projet_votes\polls\tests_admin.py", line 137, in test_adm_options_update
    self.assertEqual(self.company.siret, "987654321")
AssertionError: '0123456789' != '987654321'
- 0123456789
+ 987654321
----------------------------------------------------------------------

Reponse is <HttpResponse status_code=200, "text/html; charset=utf-8">

Your post is not working properly.
Response-of-post.content, can help you with it .
You need to check if the problem is because you don’t send all the required fields or because the data it self not assigned in the right manner.

You also can use request factory.

I tried to display values of each form’s field, and it appears that some of them are still populated, some other are not. I really do not understand why, and as soon as mandatory fields are empty, the form is not valid, so no changes are made.
Thus, I presume the problem comes from my tests, it can even be a very small bullet but I can’t find out what’s wrong. I don’t know how to really use the context, as it’s a quite big dictionary, and except the form’s fields, I do not really know what I would check.

How could request factory help me? I never used this feature and reading the documentation did not give me a clue no how to use it (but why no if it can help).

Thanks again for your support.

Dear all,
I finally got the solution.
The problem came from several points:

  • it’s necessary to post all fields from the form
  • the imagefield should be mocked
  • there is an issue with nullable / blankable fields and I need to populate at least the numbers (I will probably open a dedicated subject).

Thus, the complete solution looks like this:

from django.core.files.uploadedfile import SimpleUploadedFile

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",
        street_num=1,         # I would like to put nothing, but it's an number and '' raises a ValueError
        street_cplt='',       # it seems mandatory to explicitely add an empty string, if not the POST raises AttributeError
        address1="Rue des fauvettes",
        address2='',          # same as street_cplt
        zip_code="99456",
        city='Somewhere',
        host="smtp.gmail.com",
        port=587,
        hname="test@polls.com",
        fax="toto",
    )

class TestOptions(TestCase):
    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
        self.company.siret = "987654321"
        response = self.client.post(
            reverse("polls:adm_options", args=[self.company.comp_slug]),
            self.company.__dict__,
        )
        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")
2 Likes