How to unit test a blanck field form in Django?

I spent quite a lot of time to set some unit test, and one of the issues was the setting of some fields that I defines to be nullable and blankable. Putting dummy values was not an issue, but I wonder: how to deal with fields that need to be blank, in particular for numbers?
Let me write as an example an extract of my code.

The model:

class Company(models.Model):
    company_name = models.CharField("nom", max_length=200)
    comp_slug = models.SlugField("slug")
    street_num = models.IntegerField("N° de rue", null=True, blank=True)
    street_cplt = models.CharField("complément", max_length=50, null=True, blank=True)
    address = models.CharField("adresse", max_length=300)

The form:

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

    class Meta:
        model = Company
        exclude = []

The view:

def adm_options(request, comp_slug):
    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())

A very simple unit test:

def create_dummy_company(name):
    return Company.objects.create(
        company_name=name,
        comp_slug=slugify(name),
        street_num=1,
        street_cplt='',
        address='dummy address'
    )

def test_adm_options_update(self):
    self.company = create_dummy_company("Test company")
    url = reverse("polls:adm_options", args=[self.company.comp_slug])
    response = self.client.get(url)
    self.assertEqual(response.status_code, 200)
    self.company.address = 'new address'
    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)

Of course, the key part is posting the form after the update. The different cases I met were the following:

  • No problem with the test as written above
  • If I omit one of the fields (street_num or street_cplt), the post raises TypeError: Cannot encode None as POST data. Did you mean to pass an empty string or omit the value? This is my main issue as far as no error should be raised if one field stays blank
  • Anyway, in this case, what about number fields? How can I set street_num to blank? If I try street_num='', it raises a Value error: ValueError: invalid literal for int() with base 10: ''

So how could I deal with that for, for example, build a unit test that checks that I can post a form with no values for each single field set to null=True, blank=True?

First, as docs indicates you should avoid to use null fields in text based fields.

In the case of the street_num field, unless you need some math with this data I think could be better to use Charfield.
But I have a question. Are you using MySql? it tends to have problems with null integers until I remember (I talk for other uses, I have never user Django + MySql)

I use SQLite for my development environment and PostgreSQL for my production server. The restriction you highlight is about Oracle and as far as I know, each rdms has it’s weaknesses (some more than others ^^).
But to be honest I have ideas for workarounds, it’s more for me to dive deeper in Django and unit testings: I have problems due to a similar situation, so I put dummy values or empty string, but is there any other way to deal with this situation? I feel it could be a very basic test to check if my model works, or a basic ModelForm works, isn’t it?

I agree with you. But I think in this kind of tests you need to use the same database in production and development, I know this is not related with your problem, but you can run test that are ok in one DB and not in other.

Returning to your problem I read again it and I think the problem is that you are testing a Django Form, not the model, in your Django CompanyForm you define all the fields of the model (“exclude” is an empty list), so you need to pass some information to the dictionary of the form as Post Data and separate this test in two tests, one for the reversed URL and other for the post data.

A think this link could be helpful.

Thanks for your reply, for me it’s really interesting.
You’re completely right, it’s the Form I would test here. As far as other issues have been corrected, I also tried again to send a dict with a partial set of data (mandatory fields are of course included) and it still does not work.

I fully agree with that, I’m aware of the limitations of my model and I could have bad surprises while pushing to production a code tested on a completely different database, I really need to trust the ORM! It’s something I would not recommend, but at the beginning this project was not supposed to be developed that much.

I also had a look at your link, as well as the definition of blank=True property and I understand that there is a slightly difference with blank=True, as far as we don’t talk about NULL values, but empty fields. But, in this case, what about numbers?
I actually had another question in mind: according to my first tests, the code worked, so I was wondering if there was a difference between the client.post() and request.post(), which is impossible to me. But, to be sure, I just tested again and… my application does not work! I enriched my tests with other fields, made more tests (I even tried to put a value for street_num), checked again… I’m a bit discouraged :-/

I’m not sure to understand what you mean: how to separate each part and what would it really change?

The more I read your post, more I realize the complexly of the problem.

First because this are “Unitary Tests”, so they should test one and only one thing. Second In that way you can separate problems.

I have been reading this ticket, and it appears that this is the behavior you should expect if you pass None values . So, I think I should try something like this:

self.client.post ( reverse ("polls: adm_options", { "company_name = name": "Test Company", "comp_slug": slugify ("Test Company"), "address": "dummy address" } )

I believe that the main difference is that “client” could have authentication and authorization, “request” not.

Hi lincal,

I think two concepts (Models vs Forms content) here are being mixed up, causing some confusion.

How to test your view with form?
Firstly to directly address your point, instead of sending the client.post(...) request with a dictionary you obtained from company.__dict__, you should probably just create a dict of query parameters - which can (should) all be strings, and thus can be empty with ''.

Why is this the right way to go about it?
If you use the Dev Tools of your browser to peek at a POST request, you will see that a request in the case of your example would be coded like this (in the scenario where fields are left empty):

comp_slug=foobar&street_num=&street_cplt=&address=dummy+address

You will see that (a) all fields are present, even empty ones, and (b) this is just a concatenated string: within the world of HTTP requests there are no data types and everything is just text.

You can mimic a client.post(...) request for the same content with

dict(comp_slug='foobar', street_num='', street_cplt='', address='dummy address')

The world of the ORM however, does have the concepts of data types and NULLs. So your ORM model can contain None, and it also understandably throws an error if you try to assign an (empty) string to an IntegerField.

So in summary, I think can address your issue by just avoiding usage of company.__dict__, and just creating a new dict representing what you would expect to see in the HTTP request (i.e. based on strings, and never containing None).

Difference between request and client

Are you referring to a request obtained from RequestFactory ? If so, as described in the docs, one of the key differences is RequestFactory requests do not pass the middleware, so for example it does not go through user authentication. RF is nice if you want to write tests for middleware you are writing, or if you have need to be very clinical about your unit tests - but otherwise I find it’s just more hassle and the Client will fill most of your needs.

1 Like