5

Testing the REST API

In software engineering, testing is a process to check whether the actual software product performs as expected and is bug free.

There are a lot of ways to test software through both manual and automated tests. But in this project, we’ll focus more on automated testing. However, we’ll first dive into the different ways of testing software, including their pros and their cons, and also talk about the concept of the testing pyramid. We’ll also check the tools needed to add tests to a Django application and add tests to the models and the viewsets. This chapter will help you understand testing for developers and also how to write tests for a Django API.

In this chapter, we’ll be covering the following topics:

  • What is testing?
  • Testing in Django
  • Configuring the testing environment
  • Writing tests for Django models
  • Writing tests for Django viewsets

Technical requirements

You can find the code of the current chapter at this link: https://github.com/PacktPublishing/Full-stack-Django-and-React/tree/chap5.

What is testing?

To make it simple, testing is finding out how well something works.

However, the process comprises a group of techniques to determine the correctness of the application under a script or manual test directly on the user interface. The aim is to detect failures, including bugs and performance issues, in the application, so that they can be corrected.

Most of the time, testing is done by comparing the software requirements to the actual software product. If one of the requirements is to make sure that input only accepts numbers and not characters or files, a test will be conducted to check whether the input has a validation system to reject non-number values in the input.

However, testing also involves an examination of code and the execution of code in various environments and conditions.

What is software testing?

Software testing is the process of examining the behavior of the software under test for validation or verification. It considers the attributes of reliability, scalability, reusability, and usability to evaluate the execution of the software components (servers, database, application, and so on) and find software bugs, errors, or defects.

Software testing has a lot of benefits, some of which are as follows:

  • Cost effectiveness: Testing any software project helps the business save money in the long run. As the process helps detect bugs and check whether newly added features are working in the system without breaking things, it’s a great technical debt reducer.
  • Security: If testing is done well, it can be a quick way to detect security risks and problems at an early stage before deploying a product to the whole world.
  • Product quality: Testing helps with performance measurement, making sure that the requirements are respected.

Why is software testing important?

Testing your software is important because it helps reduce the impact of bugs through bug identification and resolution. Some bugs can be quite dangerous and can lead to financial losses or endanger human life. Here are some historical examples:

Source: https://lexingtontechnologies.ng/software-testing/.

  • In April 1999, $1.2 billion were lost due to the failure of a military satellite launch. To date, this is the costliest accident in the history of the world.
  • In 2014, the giant Nissan recalled over 1 million cars from the market because of a software failure in the airbag sensory detectors.
  • In 2014, some of Amazon's third-party retailers lost a lot of money because of a software glitch. The bug affected the price of the products, reducing them to 1p.
  • In 2015, a software failure in the Point of sales (POS) system of Starbucks stores caused the temporary closure of more than 60% of their stores in the US and Canada.
  • In 2015, an F-35 fighter plane fell victim to a software bug, which prevented it from detecting or identifying targets correctly. The sensor on the plane was unable to identify threats even from their own planes.
  • In 2016, Google reported a bug affecting Windows 10 machines. The vulnerability allowed users to escape security sandboxes through a flow in the win32k system.

What are the various types of testing?

Testing is typically classified into three categories:

  • Functional testing: This type of testing comprises unit, integration, user acceptance, globalization, internationalization testing, and so on
  • Non-functional testing: This type of testing checks for factors such as performance, volume, scalability, usability, and load
  • Maintenance testing: This type of testing considers regression and maintenance

However, these tests can also be classified into two different types:

  • Automated tests
  • Manual tests

First, let’s see what manual testing is.

Understanding manual testing

Manual testing is the process of testing software manually to find defects or bugs. It’s the process of testing the functionalities of an application without the help of automation tools.

An example of manual testing is when test users are called to test an application or a special feature. They can be asked to test a specific form, push the application to its limits when it comes to performance, and much more.

Manual testing has a lot of advantages:

  • It’s very useful to test user interface designs and interactions
  • It’s easier to learn for new testers
  • It takes user experience and usability into consideration
  • It’s cost-effective

However, manual testing also has some cons:

  • It requires human resources.
  • It’s time-consuming.
  • Testers consider test cases based on their skills and experience. This means that a beginner tester may not cover all the functions.

Even if manual testing sounds very appealing, it can be quite a time- and resource-consuming exercise, and developers definitely do not make really good manual testers. Let’s see how automated testing can erase the cons of manual testing and place better development at the center of testing.

Understanding automated testing

Automated testing is simply the process of testing software using automation tools to find defects. These automation tools can be scripts written in the language used to build the application or some software or drivers (such as Selenium, WinRunner, and LoadRunner) to make automated testing easier and faster.

Automated testing fixes the cons of manual testing, and it also has more advantages, as shown in the following list:

  • Faster in execution
  • Cheaper than manual testing in the long run
  • More reliable, powerful, and versatile
  • Very useful in regression testing
  • Able to provide better test coverage
  • Possible to run without human intervention
  • Much cheaper

However, automated testing is also inconvenient in some ways:

  • It is expensive at the beginning
  • It has a huge cost of maintenance when requirements change
  • Automated testing tools are expensive

The real value of automated testing and manual testing comes when each is used in the right environment.

For example, manual testing is much more useful on frontend projects where you want to test the usability and user experience. Automated testing can be useful to test methods or functions in the code and is very useful for finding bugs or security issues.

In this chapter, we’ll focus on writing automated tests in Python. As we are developing an API, we want to make sure that the system is reliable and behaves as we want it to, but it should also be secure against the possible issues of the next added feature.

This said, let’s talk about testing in Django and introduce the notion of test-driven development (TDD).

Testing in Django

Testing in Python, particularly in Django, is very simple and easy. The framework actually provides many tools and utilities you can use to write tests for the models, serializers, or views in the application.

However, the Python ecosystem for testing relies a lot on one tool to write tests, and this tool has deep integration with Django. The tool is named Pytest (https://docs.pytest.org) and is a framework for writing small and readable tests. Used with Django, Pytest is mainly used for API testing by writing code to test API endpoints, databases, and user interfaces.

But why use Pytest? Well, it has the following advantages:

  • It is free and open source
  • It has a simple syntax and is very easy to start with
  • It automatically detects test files, functions, and classes
  • It can run multiple tests in parallel, increasing the performance and the speed of running tests

We’ll use Pytest in this project to write two kinds of tests: integration tests and unit tests.

Before starting to code, let’s learn about integration testing and unit testing by considering the concepts of TDD and the testing pyramid.

The testing pyramid

The testing pyramid is a framework that can help developers start with testing to create high-quality software. Basically, the testing pyramid specifies the types of tests that should be included in an automated test suite.

First of all, remember that the testing pyramid operates at three levels:

  • Unit tests
  • Integration tests
  • End-to-end tests

The following figure shows the positions of each of these levels in the pyramid and how they are prioritized in terms of the speed performance and level of integration or isolation:

Figure 5.1 – The testing pyramid

Figure 5.1 – The testing pyramid

In the preceding figure, the base level is occupied by unit testing. Unit tests target individual components or functionality to check whether they work as expected in isolated conditions. In our backend project, an example would be to test whether the like_post method on the User class model actually performs as intended. We are not testing the whole User model; we are testing one method of the User model class.

It’s definitely a good habit to write a lot of unit tests. They should comprise at least 60% of all the tests in your code base because they are fast, short, and test a lot of components.

On the second level, you have integration tests. If unit tests verify small pieces of a code base, integration tests test how this code interacts with other code or other parts of the software. A useful, albeit controversial, example of integration testing is writing a test for a viewset. When testing a viewset, you are also testing the permissions, the authentication classes, the serializers, the models, and the database if possible. It’s a test of how the different parts of the Django API work together.

An integration test can also be a test between your application and an external service, a payment API, for example.

On the third level at the top of the pyramid, you have end-to-end tests. These kinds of tests ensure that the software is working as required. They test how the application works from beginning to end.

In this book, we’ll focus on unit and integration testing. Note that integration tests are the subject of some misunderstandings that will be cleared up once we define them. According to my personal experience, unit tests in Django are written more on the model and serializer side of each application. They can be used for testing the creation of an object in the database as well as for retrieving, updating, or deletion.

Regarding viewset tests, I believe that they can act as integration tests because running them calls on permissions, authentication, serializers, validation, and also models, depending on the action you are performing.

Returning to unit tests, they are more effective when using TDD, which comprises software development practices that focus on writing unit test cases before developing the feature. Even if it sounds counter-intuitive, TDD has a lot of advantages:

  • It ensures optimized code
  • It ensures the application of design patterns and better architecture
  • It helps the developer understand the business requirements
  • It makes the code flexible and easier to maintain

However, we didn’t particularly respect the TDD rule in the book. We relied on the Django shell and a client to test the feature of the REST API we are building. For the next features that will be added to the project, tests will be written before coding the feature.

With concepts such as TDD, unit and integration testing, and testing pyramid understood, we can now configure the testing environment.

Configuring the testing environment

Pytest, taken alone, is simply a Python framework to write unit tests in Python programs. Thankfully, there is a plugin for Pytest to write tests in Django projects and applications.

Let’s install and configure the environment for testing by using the following command:

pip install pytest-django

Once the package is installed, create a new file called pytest.ini at the root of the Django project:

pytest.ini

[pytest]
DJANGO_SETTINGS_MODULE=CoreRoot.settings
python_files=tests.py test_*.py *_tests.py

Once it’s done, run the pytest command:

pytest

You’ll see the following output:

======================== test session starts ============================
platform linux -- Python 3.10.2, pytest-7.0.1, pluggy-1.0.0
django: settings: CoreRoot.settings (from ini)
rootdir: /home/koladev/PycharmProjects/Full-stack-Django-and-React, configfile: pytest.ini
plugins: django-4.5.2
collected 0 items

Great! Pytest is installed in the project, and we can write the first test in the project to test the configuration.

Writing your first test

The Pytest environment is configured, so let’s see how we can write a simple test using Pytest.

At the root of the project, create a file called tests.py. We’ll simply write a test to test the sum of a function.

Following the TDD concept, we’ll write the test first and make it fail:

tests.py

def test_sum():
   assert add(1, 2) == 3

This function is written to check for a condition, justifying the usage of the assert Python keyword. If the condition after the assert is true, the script will continue or stop the execution. If that’s not the case, an assertion error will be raised.

If you run the pytest command, you’ll receive the following output:

Figure 5.2 – Failing tests

Figure 5.2 – Failing tests

From the preceding output, we are sure that the test has failed. Let’s now write the feature to pass the test.

In the same file, tests.py, add the following function:

tests.py

def add(a, b):
   return a + b
def test_sum():
   assert sum(1, 2) == 3

Now, run the pytest command again in the terminal. Everything should now be green:

Figure 5.3 – Test passes successfully

Figure 5.3 – Test passes successfully

Great! You have written the first test in the project using Pytest. In the next section, we’ll be writing tests for the models of the project.

Writing tests for Django models

When applying testing to a Django project, it’s always a good idea to start with writing tests for the models. But why test the models?

Well, it gives you better confidence in your code and the connections to the database. It’ll make sure that methods or attributes on the model are well represented in the database, but it can also help you with better code structure, resolving bugs, and building documentation.

Without further ado, let’s start by writing tests for the User model.

Writing tests for the User model

Inside the core/user directory, create a new file called tests.py. We’ll write tests to create a user and a simple user:

core/user/tests.py

import pytest
from core.user.models import User
data_user = {
   "username": "test_user",
   "email": "[email protected]",
   "first_name": "Test",
   "last_name": "User",
   "password": "test_password"
}

Once the imports and the data to create the user have been added, we can write the test function:

core/user/tests.py

@pytest.mark.django_db
def test_create_user():
   user = User.objects.create_user(**data_user)
   assert user.username == data_user["username"]
   assert user.email == data_user["email"]
   assert user.first_name == data_user["first_name"]
   assert user.last_name == data_user["last_name"]

Above the test_create_user function, you’ll probably notice some syntax. It’s called a decorator, and it’s basically a function that takes another function as its argument and returns another function.

@pytest.mark.django_db gives us access to the Django database. Try to remove this decorator and run the tests.

You’ll get an error output with a similar message at the end:

=================================================================== short test summary info ===================================================================
FAILED core/user/tests.py::test_create_user - RuntimeError: Database access not allowed, use the "django_db" mark, or the "db" or "transactional_db" fixture...

Well, re-add the decorator and run the pytest command and all tests should pass normally.

Let’s do another test to make sure that the creation of superuser works perfectly.

Add a new dictionary containing the data needed to create superuser:

core/user/tests.py

data_superuser = {
   "username": "test_superuser",
   "email": "[email protected]",
   "first_name": "Test",
   "last_name": "Superuser",
   "password": "test_password"
}

And here’s the function that tests the creation of superuser:

core/user/tests.py

@pytest.mark.django_db
def test_create_superuser():
   user = User.objects.create_superuser(**data_superuser)
   assert user.username == data_superuser["username"]
   assert user.email == data_superuser["email"]
   assert user.first_name == data_superuser["first_name"]
   assert user.last_name == data_superuser["last_name"]
   assert user.is_superuser == True
   assert user.is_staff == True

Run the tests again, and everything should be green.

Great! Now that we have a better understanding of how pytest works for tests, let’s write tests for the Post model.

Writing tests for the Post model

To create a model, we need to have a user object ready. This will also be the same for the Comment model. To avoid repetition, we’ll simply write fixtures.

A fixture is a function that will run before each test function to which it’s applied. In this case, the fixture will be used to feed some data to the tests.

To add fixtures in the project, create a new Python package called fixtures in the core directory.

In the core/fixtures directory, create a file called user.py. This file will contain a user fixture:

core/fixtures/user.py

import pytest
from core.user.models import User
data_user = {
   "username": "test_user",
   "email": "[email protected]",
   "first_name": "Test",
   "last_name": "User",
   "password": "test_password"
}
@pytest.fixture
def user(db) -> User:
   return User.objects.create_user(**data_user)

In the preceding code, the @pytest.fixture decorator labels the function as a fixture. We can now import the user function in any test and pass it as an argument to the test function.

Inside the core/post directory, create a new file called tests.py. This file will then test for the creation of a post.

Here’s the code:

core/post/tests.py

import pytest
from core.fixtures.user import user
from core.post.models import Post
@pytest.mark.django_db
def test_create_post(user):
   post = Post.objects.create(author=user,
                              body="Test Post Body")
   assert post.body == "Test Post Body"
   assert post.author == user

As you can see, we are importing the user function from user.py in the fixtures directory and passing it as an argument to the test_create_post test function.

Run the pytest command, and everything should be green.

Now that we have a working test for the Post model, let’s write tests for the Comment model.

Writing tests for the Comment model

Writing tests for the Comment model requires the same steps as the tests for the Post model. First of all, create a new file called post.py in the core/fixtures directory.

This file will contain the fixture of a post, as it’s needed to create a comment.

But the post fixture will also need a user fixture. Thankfully, it’s possible with Pytest to inject fixtures into other fixtures.

Here’s the code for the post fixture:

core/fixtures/post.py

import pytest
from core.fixtures.user import user
from core.post.models import Post
@pytest.fixture
def post(db, user):
   return Post.objects.create(author=user,
                              body="Test Post Body")

Great! With the fixtures added, we can now write the test for comment creation.

Inside the core/comment/ directory, create a new file called tests.py:

core/comment/tests.py

import pytest
from core.fixtures.user import user
from core.fixtures.post import post
from core.comment.models import Comment
@pytest.mark.django_db
def test_create_comment(user, post):
   comment = Comment.objects.create(author=user, post=post,
     body="Test Comment Body")
   assert comment.author == user
   assert comment.post == post
   assert comment.body == "Test Comment Body"

Run the tests with the pytest command, and everything should be green.

Great! We’ve just written tests for all the models in the project. Let’s move on to writing tests for the viewsets.

Writing tests for your Django viewsets

Viewsets or endpoints are the interfaces of the business logic that the external clients will use to fetch data and create, modify, or delete data. It’s always a great habit to have tests to make sure that the whole system, starting from a request to the database, is working as intended.

Before starting to write the tests, let’s configure the Pytest environment to use the API client from DRF.

The API client is a class that handles different HTTP methods, as well as features such as authentication in testing, which can be very helpful for directly authenticating without a username and password to test some endpoints. Pytest provides a way to add configurations in a testing environment.

Create a file named conftest.py at the root of the project. Inside the file, we’ll create a fixture function for our custom client:

conftest.py

import pytest
from rest_framework.test import APIClient
@pytest.fixture
def client():
   return APIClient()

Great! We can now directly call this client in the next tests.

Let’s start by testing the authentication endpoints.

Writing tests for authentication

Inside the core/auth directory, create a file named tests.py. Instead of writing test functions directly, we write a class that will contain the testing methods as follows:

core/auth/tests.py

import pytest
from rest_framework import status
from core.fixtures.user import user
class TestAuthenticationViewSet:
   endpoint = '/api/auth/'

Let’s add the test_login method to the TestAuthenticationViewSet class:

Core/auth/tests.py

...
   def test_login(self, client, user):
       data = {
           "username": user.username,
           "password": "test_password"
       }
       response = client.post(self.endpoint + "login/",
                              data)
       assert response.status_code == status.HTTP_200_OK
       assert response.data['access']
       assert response.data['user']['id'] ==
         user.public_id.hex
       assert response.data['user']['username'] ==
         user.username
       assert response.data['user']['email'] == user.email
  ...

This method basically tests the login endpoint. We are using the client fixture initialized in the conftest.py file to make a post request. Then, we test for the value of status_code of the response and the response returned.

Run the pytest command, and everything should be green.

Let’s add tests for the register and refresh endpoints:

core/auth/tests.py

...
   @pytest.mark.django_db
   def test_register(self, client):
       data = {
           "username": "johndoe",
           "email": "[email protected]",
           "password": "test_password",
           "first_name": "John",
           "last_name": "Doe"
       }
       response = client.post(self.endpoint + "register/",
                              data)
       assert response.status_code ==
         status.HTTP_201_CREATED
   def test_refresh(self, client, user):
      data = {
           "username": user.username,
           "password": "test_password"
       }
       response = client.post(self.endpoint + "login/",
                                data)
       assert response.status_code == status.HTTP_200_OK
       data_refresh = {
           "refresh":  response.data['refresh']
       }
       response = client.post(self.endpoint + "refresh/",
                              data_refresh)
       assert response.status_code == status.HTTP_200_OK
       assert response.data['access']

In the preceding code, within the test_refresh method, we log in to get a refresh token to make a request to get a new access token.

Run the pytest command again to run the tests, and everything should be green.

Let’s move on to writing tests for PostViewSet.

Writing tests for PostViewSet

Before starting to write the viewsets tests, let’s quickly refactor the code to simply write the tests and follow the DRY rule. Inside the core/post directory, create a Python package called tests. Once it’s done, rename the tests.py file in the core/post directory to test_models.py and move it to the core/post/tests/ directory.

Inside the same directory, create a new file called test_viewsets.py. This file will contain tests for PostViewSet:

core/post/tests/test_viewsets.py

from rest_framework import status
from core.fixtures.user import user
from core.fixtures.post import post
class TestPostViewSet:
   endpoint = '/api/post/'

PostViewSet handles requests for two types of users:

  • Authenticated users
  • Anonymous users

Each type of user has different permissions on the post resource. So, let’s make sure that these cases are handled:

core/post/tests/test_viewsets.py

...
   def test_list(self, client, user, post):
       client.force_authenticate(user=user)
       response = client.get(self.endpoint)
       assert response.status_code == status.HTTP_200_OK
       assert response.data["count"] == 1
   def test_retrieve(self, client, user, post):
       client.force_authenticate(user=user)
       response = client.get(self.endpoint +
                             str(post.public_id) + "/")
       assert response.status_code == status.HTTP_200_OK
       assert response.data['id'] == post.public_id.hex
       assert response.data['body'] == post.body
       assert response.data['author']['id'] ==
         post.author.public_id.hex

For these tests, we are forcing authentication. We want to make sure that authenticated users have access to the post’s resources. Let’s now write a test method for post creation, updating, and deletion:

core/post/tests/test_viewsets.py

...
   def test_create(self, client, user):
       client.force_authenticate(user=user)
       data = {
           "body": "Test Post Body",
           "author": user.public_id.hex
       }
       response = client.post(self.endpoint, data)
       assert response.status_code ==
         status.HTTP_201_CREATED
       assert response.data['body'] == data['body']
       assert response.data['author']['id'] ==
         user.public_id.hex
   def test_update(self, client, user, post):
       client.force_authenticate(user=user)
       data = {
           "body": "Test Post Body",
           "author": user.public_id.hex
       }
       response = client.put(self.endpoint +
         str(post.public_id) + "/", data)
       assert response.status_code == status.HTTP_200_OK
       assert response.data['body'] == data['body']
   def test_delete(self, client, user, post):
       client.force_authenticate(user=user)
       response = client.delete(self.endpoint +
         str(post.public_id) + "/")
       assert response.status_code ==
         status.HTTP_204_NO_CONTENT

Run the tests, and the outcomes should be green. Now, for the anonymous users, we want them to access the resource in reading mode, so they can’t create, modify, or delete a resource. Let’s test and validate these features:

core/post/tests/test_viewsets.py

...
   def test_list_anonymous(self, client, post):
       response = client.get(self.endpoint)
       assert response.status_code == status.HTTP_200_OK
       assert response.data["count"] == 1
   def test_retrieve_anonymous(self, client, post):
       response = client.get(self.endpoint +
         str(post.public_id) + "/")
       assert response.status_code == status.HTTP_200_OK
       assert response.data['id'] == post.public_id.hex
       assert response.data['body'] == post.body
       assert response.data['author']['id'] ==
         post.author.public_id.hex

Run the tests to make sure everything is green. After that, let’s test the forbidden methods:

core/post/tests/test_viewsets.py

...
def test_create_anonymous(self, client):
       data = {
           "body": "Test Post Body",
           "author": "test_user"
       }
       response = client.post(self.endpoint, data)
       assert response.status_code ==
         status.HTTP_401_UNAUTHORIZED
   def test_update_anonymous(self, client, post):
       data = {
           "body": "Test Post Body",
           "author": "test_user"
       }
       response = client.put(self.endpoint +
         str(post.public_id) + "/", data)
       assert response.status_code ==
         status.HTTP_401_UNAUTHORIZED
   def test_delete_anonymous(self, client, post):
       response = client.delete(self.endpoint +
         str(post.public_id) + "/")
       assert response.status_code ==
         status.HTTP_401_UNAUTHORIZED

Run the tests again. Great! We’ve just written tests for the post viewset. You should now have a better understanding of testing with viewsets.

Let’s quickly write tests for CommentViewSet.

Writing tests for CommentViewSet

Before starting to write the viewset tests, let’s also quickly refactor the code for writing the tests. Inside the core/comment directory, create a Python package called tests. Once it’s done, rename the tests.py file in the core/post directory to test_models.py and move it to the core/comment/tests/ directory.

Inside the same directory, create a new file called test_viewsets.py. This file will contain tests for CommentViewSet.

Just like in PostViewSet, we have two types of users, and we want to write test cases for each of their permissions.

However, before creating comments, we need to add comment fixtures. Inside the core/fixtures directory, create a new file called comment.py and add the following content:

core/fixtures/comment.py

import pytest
from core.fixtures.user import user
from core.fixtures.post import post
from core.comment.models import Comment
@pytest.fixture
def comment(db, user, post):
   return Comment.objects.create(author=user, post=post,
                                 body="Test Comment Body")

After that, inside core/comment/tests/test_viewsets.py, add the following content first:

core/comment/tests/test_viewsets.py

from rest_framework import status
from core.fixtures.user import user
from core.fixtures.post import post
from core.fixtures.comment import comment
class TestCommentViewSet:
   # The comment resource is nested under the post resource
   endpoint = '/api/post/'

Next, let’s add tests to the list and retrieve comments as authenticated users:

core/comment/tests/test_viewsets.py

...
def test_list(self, client, user, post, comment):
       client.force_authenticate(user=user)
       response = client.get(self.endpoint +
         str(post.public_id) + "/comment/")
       assert response.status_code == status.HTTP_200_OK
       assert response.data["count"] == 1
   def test_retrieve(self, client, user, post, comment):
       client.force_authenticate(user=user)
       response = client.get(self.endpoint +
                             str(post.public_id) +
                             "/comment/" +
                             str(comment.public_id) + "/")
       assert response.status_code == status.HTTP_200_OK
       assert response.data['id'] == comment.public_id.hex
       assert response.data['body'] == comment.body
       assert response.data['author']['id'] ==
         comment.author.public_id.hex

Make sure that these tests pass by running the pytest command. The next step is to add tests for comment creation, updating, and deletion:

core/comment/tests/test_viewsets.py

...
    def test_create(self, client, user, post):
       client.force_authenticate(user=user)
       data = {
           "body": "Test Comment Body",
           "author": user.public_id.hex,
           "post": post.public_id.hex
       }
       response = client.post(self.endpoint +
         str(post.public_id) + "/comment/", data)
       assert response.status_code ==
         status.HTTP_201_CREATED
       assert response.data['body'] == data['body']
       assert response.data['author']['id'] ==
         user.public_id.hex
   def test_update(self, client, user, post, comment):
       client.force_authenticate(user=user)
       data = {
           "body": "Test Comment Body Updated",
           "author": user.public_id.hex,
           "post": post.public_id.hex
       }
       response = client.put(self.endpoint +
                             str(post.public_id) +
                             "/comment/" +
                             str(comment.public_id) +
                             "/", data)
       assert response.status_code == status.HTTP_200_OK
       assert response.data['body'] == data['body']
   def test_delete(self, client, user, post, comment):
       client.force_authenticate(user=user)
       response = client.delete(self.endpoint +
         str(post.public_id) + "/comment/" +
         str(comment.public_id) + "/")
       assert response.status_code ==
         status.HTTP_204_NO_CONTENT

Run the tests again to make sure everything is green. Let’s write tests for the anonymous users now.

First of all, we need to make sure that they can access the resources with the GET method:

core/comment/tests/test_viewsets.py

...
   def test_list_anonymous(self, client, post, comment):
       response = client.get(self.endpoint +
                             str(post.public_id) +
                             "/comment/")
       assert response.status_code == status.HTTP_200_OK
       assert response.data["count"] == 1
   def test_retrieve_anonymous(self, client, post,
     comment):
       response = client.get(self.endpoint +
         str(post.public_id) + "/comment/" +
         str(comment.public_id) + "/")
       assert response.status_code == status.HTTP_200_OK

Next, we need to make sure that an anonymous user can’t create, update, or delete a comment:

core/comment/tests/test_viewsets.py

   def test_create_anonymous(self, client, post):
       data = {}
       response = client.post(self.endpoint +
         str(post.public_id) + "/comment/", data)
       assert response.status_code ==
         status.HTTP_401_UNAUTHORIZED
   def test_update_anonymous(self, client, post, comment):
       data = {}
       response = client.put(self.endpoint +
         str(post.public_id) + "/comment/" +
         str(comment.public_id) + "/", data)
       assert response.status_code ==
         status.HTTP_401_UNAUTHORIZED
   def test_delete_anonymous(self, client, post, comment):
       response = client.delete(self.endpoint +
         str(post.public_id) + "/comment/" +
         str(comment.public_id) + "/")
       assert response.status_code ==
         status.HTTP_401_UNAUTHORIZED

In the preceding cases, the data dict is empty because we are expecting error statuses.

Run the tests again to make sure that everything is green!

And voilà. We’ve just written tests for CommentViewSet. We also need to write tests for the UserViewSet class, but this will be a small project for you.

Writing tests for the UserViewSet class

In this section, let’s do a quick hands-on exercise. You’ll write the code for the UserViewSet class. It’s quite similar to the other tests we’ve written for PostViewSet and CommentViewSet. I have provided you with the structure of the class, and all you have to do is to write the testing logic in the methods. The following is the structure you need to build on:

core/user/tests/test_viewsets.py

from rest_framework import status
from core.fixtures.user import user
from core.fixtures.post import post
class TestUserViewSet:
   endpoint = '/api/user/'
   def test_list(self, client, user):
       pass
   def test_retrieve(self, client, user):
       pass
   def test_create(self, client, user):
       pass
   def test_update(self, client, user):
       pass

Here are the requirements concerning the tests:

  • test_list: An authenticated user should enable a list of all users
  • test_retrieve: An authenticated user can retrieve resources concerning a user
  • test_create: Users cannot create users directly with a POST request
  • test_update: An authenticated user can update a user object with a PATCH request

You can find the solution to this exercise here: https://github.com/PacktPublishing/Full-stack-Django-and-React/blob/main/core/user/tests/test_viewsets.py.

Summary

In this chapter, we learned about testing, the different types of testing, and their advantages. We also introduced testing in Django using Pytest and wrote tests for the models and viewsets. These skills acquired in writing tests using the TDD method help you better design your code, prevent bugs tied to code architecture, and improve the quality of the software. Not to forget, they also give you a competitive advantage in the job market.

This is the last chapter of Part 1, Technical Background. The next part will be dedicated to React and connecting the frontend to the REST API we’ve just built. In the next chapter, we’ll learn more about frontend development and React, and we’ll also create a React project and run it.

Questions

  1. What is testing?
  2. What is a unit test?
  3. What is the testing pyramid?
  4. What is Pytest?
  5. What is a Pytest fixture?
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset