Django: Testing The What, Why and How. (Code)
Tags: django, python, testing, unittest
Published: 2021-07-05 11:49:03

This is a continuation of two part series, please check out [part-1] (https://dev.to/nerdy_picasso/django-testing-the-what-why-and-how-1d4c) to look at the theoretical part of testing.

## Table Of Contents
1. [Coverage] (#coverage)
2. [Testing Models] (#testing-models)
3. [Testing Views] (#testing-views)
4. [Testing Forms] (#testing-forms)
5. [Next Topics To Cover] (#next-topics-to-cover)
6. [Conclusion] (#conclusion)

In this blog we will be looking at how to start with testing and later on how to implement.

<br>
[Our project setup] (https://dev.to/nerdy_picasso/django-testing-the-what-why-and-how-1d4c#our-testing-project)

## Coverage
First we need to know what needs to be tested. As I said before, there is **no need to test Django' vanilla model or Django ORM and python's in-built functions**.
This is where coverage comes in handy, it is a convenient way for a beginner to find out the functions we need to test.

<br>
[How to setup coverage](https://dev.to/nerdy_picasso/django-testing-the-what-why-and-how-1d4c#coverage)

## Testing Models
let's open up `index.html` in our newly created `htmlscov` folder in our project:
```book-library > htmlscov > index.html```

We will see something like this:
![Screenshot_2021-06-28_13-08-04](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ogwljj1m6fohry4j6msx.png)

Let's go to our `catalog/models.py`. 
![Screenshot_2021-06-28_13-13-12](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9pde8ymjbujgud019srz.png)

The red line shows the line we need to test and the percentage represents how much code has been covered with testing.

Considering our `Book` model:
```python
class Book(models.Model):
    """Model representing a book
    """
    title = models.CharField(max_length=200)
    author = models.ForeignKey('Author', on_delete=models.SET_NULL, null=True)
    summary = models.TextField(
        max_length=1000, help_text='Enter a brief description of the book')
    isbn = models.CharField('ISBN', max_length=13, unique=True,
                            help_text='13 Character <a href="https://www.isbn-international.org/content/what-isbn">ISBN number</a>')
    genre = models.ManyToManyField(Genre, help_text='Select a genre for this book', related_name='books')

    def __str__(self):
        return self.title

    def get_absolute_url(self):
        """Returns the url to access a detail record for this book.
        """
        return reverse('book-detail', args=[str(self.id)])
```

### Test Model Implementation
```python
class TestModels(TestCase):
    @classmethod
    def setUpTestData(self):
        thriller = Genre.objects.create(name='thriller')
        scifi = Genre.objects.create(name='scifi')
        book = Book.objects.create(
            title='test title',
            summary='test summary',
            isbn='test isbn',
        )
        book.genre.set([thriller.pk, scifi.pk])
    
   def test_book_has_genre(self):
        """checks for genres in book
        """
        book = Book.objects.first()
        self.assertEqual(book.genre.count(), 2)

    def test_book_str(self):
        """Checks str for book
        """
        book = Book.objects.first()
        self.assertEqual(str(book), "test title")

    def test_book_absolute_url_with_200(self):
        book = Book.objects.first()
        self.assertEqual(book.get_absolute_url(), '/catalog/book/1')
        response = self.client.get(book.get_absolute_url())
        self.assertEqual(response.status_code, 200)

```

### Testing str
First we setup our data and then we test for the `str` representation.

```python
    def test_book_str(self):
        """Checks str for book
        """
        book = Book.objects.first()
        self.assertEqual(str(book), "test title")
```

One thing to note is the test method name should always start with `test_` and it should be precise so that one can understand what the test is about. However, in cases where you feel that the naming may not be precise or meaningful make sure to use docstring to explain what it is supposed to do. 

### Testing Many To Many
```python
    def test_book_has_genre(self):
        """checks for genres in book
        """
        book = Book.objects.first()
        self.assertEqual(book.genre.count(), 2)
```
We assigned two genres for our book when we were creating our book. We are simply checking for the count of genres in the book, which tells us that the genres have been assigned successfully.

### Testing Absolute Url
```python
    def test_book_absolute_url_with_200(self):
        book = Book.objects.first()
        self.assertEqual(book.get_absolute_url(), '/catalog/book/1')
        response = self.client.get(book.get_absolute_url())
        self.assertEqual(response.status_code, 200)
```
In our `Book` model we have a method called `get_absolute_url` which gets the information of a single book. We check if the status code is 200 and the `get_absolute_url` method actually is the correct url.

## Testing Views
To validate our views we will make use of [Django's test client] (https://dev.to/nerdy_picasso/django-testing-the-what-why-and-how-1d4c#client).
***Note: This is not strictly "unit test". It is actually quite difficult to write independent unit test for Django view code.***

We can check for multiple things in our views but these are some common ones.
1. Check if only the logged in user can authorize your view (If user needs to be logged in to access).
2. Check if the correct user can authorize your view.
3. Check if the both `GET` and `POST` is working the way it should.
4. Check for the validation you may have implemented.

We will refer to our `index` view for our testing.
```python
from .models import Book, Author, BookInstance, Genre
from django.contrib.auth.decorators import login_required

@login_required(login_url='/login/')
def index(request):
    """View function for home page of site."""

    # Generate counts of some of the main objects
    num_books = Book.objects.all().count()
    num_instances = BookInstance.objects.all().count()

    # Available books (status = 'a')
    num_instances_available = BookInstance.objects.filter(status__exact='a').count()
    num_authors = Author.objects.count()

    # Number of visits to this view, as counted in the session variable.
    num_visits = request.session.get('num_visits', 0)
    request.session['num_visits'] = num_visits + 1

    context = {
        'num_books': num_books,
        'num_instances': num_instances,
        'num_instances_available': num_instances_available,
        'num_authors': num_authors,
        'num_visits': num_visits,
    }

    # Render the HTML template index.html with the data in the context variable
    return render(request, 'index.html', context=context)
``` 
### Test View Implementation
```python
from django.test import TestCase
from catalog.models import Author
from django.urls import reverse
from django.contrib.auth.models import User
from model_bakery import baker


class IndexViewTest(TestCase):
    @classmethod
    def setUpTestData(cls):
        User.objects.create_user(username='test_user', password='test_password')
        books = baker.make('catalog.Book', _quantity=10)

    def test_view_deny_anonymous(self):
        """Test the view for unauthenticated user if unauthenticated will redirect to login page
        """
        response = self.client.get('/catalog/')
        self.assertRedirects(response, '/login/?next=/catalog/')
        response = self.client.post('/catalog/')
        self.assertRedirects(response, '/login/?next=/catalog/')

    def test_view_url_accesible_by_name(self):
        """Test view is accesible by the reverse method
        """
        self.client.login(username='test_user', password='test_password')
        response = self.client.get(reverse('index'))
        self.assertEqual(response.status_code, 200)
        response = self.client.post(reverse('index'))
        self.assertEqual(response.status_code, 200)

    def test_view_uses_correct_template_(self):
        """Test view is using correct template
        """
        self.client.login(username='test_user', password='test_password')
        response = self.client.get(reverse('index'))
        self.assertEqual(response.status_code, 200)
        self.assertTemplateUsed(response, 'index.html')

    def test_view_has_context_num_books(self):
        self.client.login(username='test_user', password='test_password')
        response = self.client.get(reverse('index'))
        self.assertEqual(response.status_code, 200)
        self.assertTrue('num_books' in response.context)
        self.assertEqual(response.context['num_books'], 10)
```
### Deny Unauthorized User
```python
    def test_view_deny_anonymous(self):
        """Test the view for unauthenticated user if unauthenticated will redirect to login page
        """
        response = self.client.get('/catalog/')
        self.assertRedirects(response, '/login/?next=/catalog/')
        response = self.client.post('/catalog/')
        self.assertRedirects(response, '/login/?next=/catalog/')
```
We are checking if the unauthenticated user is being denied and redirected to the login page.

### URL Is Accessed By Name
```python
    def test_view_url_accessible_by_name(self):
        """Test view is accessible by the reverse method
        """
        self.client.login(username='test_user', password='test_password')
        response = self.client.get(reverse('index'))
        self.assertEqual(response.status_code, 200)
        response = self.client.post(reverse('index'))
        self.assertEqual(response.status_code, 200)
```

We check if the `reverse` method is working and gets us a status code `200` for both `GET` and `POST` method.

### Correct Template Usage
```python
    def test_view_uses_correct_template_(self):
        """Test view is using correct template
        """
        self.client.login(username='test_user', password='test_password')
        response = self.client.get(reverse('index'))
        self.assertEqual(response.status_code, 200)
        self.assertTemplateUsed(response, 'index.html')
```

This checks if the correct template is being used by using `assertTemplateUsed` method.

### Test `num_books`
```python
    def test_view_has_context_num_books(self):
        self.client.login(username='test_user', password='test_password')
        response = self.client.get(reverse('index'))
        self.assertEqual(response.status_code, 200)
        self.assertTrue('num_books' in response.context)
        self.assertEqual(response.context['num_books'], 10)
```
In this method we test if the number of books are assigned correctly. We use [model-bakery] (https://model-bakery.readthedocs.io/en/latest/) here to create 10 `Book` instances.

## Testing Forms
Forms are something that can be easily unit tested. We can test for the validation in forms. They accept dictionary of values, validate it and return either errors or cleaned data.
In our project we do not have a form, so let's make one for this tutorial's sake.

```python
from django import forms
from .models import Book


class AddBookForm(forms.ModelForm):
    class Meta:
        model = Book
        fields = ["title"]

    def clean_title(self):
        title = self.cleaned_data['title']
        if not title:
            return title
        if not title[0].isupper():
            self.add_error("title", "Should start with an uppercase")
        if title.endswith("."):
            self.add_error("title", "Should not end with a full stop")

        return title
```

We included some validation in our model which we will be testing.

Our testing would look something like this.

```python
from django.test import TestCase
from catalog.forms import AddBookForm


class AddBookFormTests(TestCase):
    def test_title_starting_lowercase(self):
        form = AddBookForm(data={"title": "a lowercase title"})

        self.assertEqual(
            form.errors["title"], ["Should start with an uppercase"]
        )

    def test_title_ending_full_stop(self):
        form = AddBookForm(data={"title": "A stopped title."})
        
        self.assertEqual(
            form.errors["title"], ["Should not end with a full stop"]
        )
    
    def test_title_with_ampersand(self):
        form = AddBookForm(data={"title": ""})
        
        self.assertEqual(
            form.errors["title"], ["This field is required."]
        )
```
As we can see in forms we mainly test for validations. If we want to add some sort of integration tests alongside unit test we could check the validation by using using client doing something like this:
 
```python
    def test_title_starting_lowercase(self):
        response = self.client.post(
            "/books/add/", data={"title": "a lowercase title"}
        )

        self.assertEqual(response.status_code, HTTPStatus.OK)
        self.assertContains(
            response, "Should start with an uppercase letter", html=True
        )
```

## Next Topics To Cover

I have tried to cover as much as possible in this series but there are tons of things out there when it comes to testing. I will try to cover these topics in my upcoming blogs. A short list includes:

- **Continuous Integration**: Automatically run all tests whenever there a new commit is made. It can be done using GitHub Actions, Travis CI, Circle CI.

- **Mock**: It is an act of replacing part of the application you are testing with a dummy version.

- **Pytest**: It is a testing framework just like `unittest`. We can also use this instead of `unittest`

- **Model Bakery**: We saw a small implementation of this in our project but there is more to discuss.

- **TDD**: Test Driven Development, this is actually quite an advance topic which takes a while to get used to, but once you start doing and understand the benefits, you will never want to go back.

## Conclusion

To conclude, there is not any concrete way to write your tests. It depends on the project you are working on or the company you are working at. Everyone can have their own way of testing their codes.

The final advice I can give you is, do not be scared of writing tests. Yes, it can be intimidating at first but once you start writing tests you will understand your codes better, it will stimulate to question yourself to think about all the cases where your program can go wrong making you a better programmer.

So this is it for today, if you have any question regarding this please do mention it in the comment or if you have any other questions please do not hesitate to contact me at the following platforms I am available on [twitter] (https://twitter.com/rabbilyasar), [GitHub] (https://github.com/rabbilyasar), [LinkedIn](https://www.linkedin.com/in/rabbilyasar/)