As a developer on a team, you have written a nice cool feature and you are so sure that it is giving the expected responses for both successful and unsuccessful requests, So you make a pull request for the team lead to review and merge but the team lead has rejected the request because it lacks testing and you are like
But you are a problem solver and set to write tests for your added feature. In this post, I will take you through how to write unit tests for applications using the Django web framework. Before we start coding let's define some terms
Testing
This is the process of ensuring that every part of your app works and communicates as expected by processing the requests as expected and giving the necessary response as the case may be for either a success or error message. Tests can be of two types, that is
Manual Testing: This is a type of testing whereby you get to test the application by using it yourself by giving it all the expected requests and seeing the responses being returned. For instance, for a payment platform, some of the features could be to register users, update user profiles, enter card details, make payments etc. So as a manual tester, you do this testing by manually using the application without the use of any script or testing software.
Software Testing: This is a type of testing whereby you get to test the application using some software tools to carry out what you would have done and do some assertion of the results to what you are expecting.
So what are the objectives to keep in mind while testing
Bugs or Defects: Writing tests is to make some bugs obvious and thus you can easily fix them before making them available for the end users.
Code Quality: When you write tests, every test written that failed will prompt the addition of code blocks to make such test pass. so there is a high chances you only write codes that are needed by your application and there are no lying around of code with no functionality i.e we write code from the test point of view (Test Driven Development)
Software Product Requirement: To ensure the application you are working on meets the required features i.e. its main functionalities. for instance for an e-commerce website, functionalities like add/remove from cart, and make payment are some of the required features that testing should help confirm that users can do without any bugs.
Fit for use: Writing tests for your application should also be to ensure that what you have added is a feature that can be used by the end users i.e. even if it meets the software requirement, is it of use to the end users?
So let's talk about some types of testing we have
Unit Testing: This involves testing each unit (functions, classes etc) of your app and ensuring each accepts and returns what is/are expected of them. For instance, ensuring the URL resolves and calls the right function, ensuring the business logic in a function does what it's supposed to do. This kind of testing is done by the developer.
Integrated Testing: This type of testing is to ensure that all of the units interact with each other as expected. For instance, ensuring the business logic interacts with the model (database processing) or the serializers.
Functional Testing (User point of view testing): This involves testing the business requirement of the application i.e. ensuring that it produces the expected result from the performed actions. This can be confused with integrated testing but it should not.
Acceptance Testing: This involves the testing of the features by a real user who did not participate in the feature development but is aware of the requirements to be met for acceptance. They require the entire application to be running while testing and focus on replicating user behaviors. But they can also go further and measure the performance of the system and reject changes if certain goals are not met.
Other types of testing we have include End-to-End testing, Performance testing, Smoke testing etc.
Now let's write some unit tests for a user registration feature making use of Django and Django rest framework. To start with let's list out the main functionalities of our registration feature from the user point of view (User Story)
As a user, I should be able to
enter my email address and password.
see error message due to wrong email and bad password format.
see an error message as a result of no data being filled for both email and password.
see a confirmation message for a successful registration.
So, we will build these features above from the test-driven development point of view.
Set up the Environment
Clone the starter files from starter files
Navigate into the clone folder and create a virtual environment using the command below
python -m venv env
- activate the virtual environment by using the command below
#mac / linux
source env/bin/activate
#windows
source env/Script/activate
- Navigate to the directory that contains manage.py file and install the requirements
pip install -r requirements.txt
- Run the test command from the command terminal and see the test result so far. Yeah it should be zero and everything should look good since no test has been written so far
python manage.py test
Open the starter files in your favorites test editor and open the testreg.py file in the test folder. So what do we have here? We have imported the necessary module to help with the testing, and TestCase for doing the main testing, The Client works as our user here to make the requests and we will be using reverse to generate our path urls from the namespace. The setUp function is to keep all the declarations that we might need for another set of test functions as each function is unaware of declarations in other functions
Let's create our first test by adding the function below to see that our registration path resolves and it calls the view function we expect it to call
from account.views import register
def test_regurl_resolves(self):
url = resolve(reverse("account:register"))
self.assertEqual(url.func, register)
Save and run the test command on the terminal i.e
python manage.py test
Oops, we have an import error of " cannot import name 'register' from 'account.views' ". let's go ahead and create that by putting the code below in account.views file and run the test command again.
def register(request):
pass
Don't forget that we write minimal code to make our test pass
Another error but a different one of " Reverse for 'register' not found. 'register' is not a valid view function or pattern name. ". Let's create our register path by updating urlpattern in the account.urls file
urlpatterns = [
path("register/", views.register, name="register") #Added line
]
now run the test command again and we should have a passed test
So in the test above, We are confident that the URL path for registration resolves and the right view function is being called.
Let's add another test to ensure we are only allowing post methods. Add the function to our test class
def test_reg_url_resolves_with_405_for_get_request(self):
response = self.client.get(self.reg_url)
self.assertEqual(response.status_code, 405)
Run the test command which should show a failed response " account.views.register didn't return an HttpResponse object. It returned None"
So let's make this pass by updating our register view function as shown below
from rest_framework.response import Response
from rest_framework import status
from rest_framework.decorators import api_view
@api_view(["POST"])
def register(request):
pass
running the test command again will give us another ok pass
Next, let's ensure that we return errors if no data i.e (email and password received). Add the function below to the test class and run the test again
def test_reg_with_no_data(self):
response = self.client.post(self.reg_url, data={})
for field in ["email", "password"]:
self.assertIn("This field is required.", response.json()[field])
Yes, another error right? " Expected a Response
, HttpResponse
or HttpStreamingResponse
to be returned from the view, but received a <class 'NoneType'>
"
Now let's take care of that...... shall we?
- We first create our model for the user by updating the models.py file to have the code below
from django.contrib.auth.models import AbstractUser
class User(AbstractUser):
email = models.EmailField(unique=True)
- We then create a file called serializers.py and put the code below
from .models import User
from rest_framework import serializers
class RegistrationSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ["email", "password"]
- Then update the registration function to have
from . import serializers
serializer = serializers.RegistrationSerializer(data = request.data)
if serializers.is_valid():
pass
return Response(serializer.errors, status = status.HTTP_400_BAD_REQUEST)
Don't forget to update your setting file of the new user table i.e add AUTH_USER_MODEL = 'account.User' to settings.py file and run makemigrations and migrate commands respectively.
woof, let's run the test command again and see what we have........... Hurray, another test passed
Now let's add a test with new user details entered but bad format
def test_urlreg_with_data(self):
wrong_passwords = ["aze12", "1234747449494", "azeezybdjhddhdhd"]
wrong_emails = ["a", "@mail.com"]
for password in wrong_passwords:
data = {"email": random.choice(wrong_emails), "password": password}
response = self.client.post(self.reg_url, data=data)
for key in ["email", "password"]:
self.assertIn(key, response.json().keys())
self.assertEqual(response.status_code, 400)
Running another test here will give us the failed assertion error of " AssertionError: 'email' not found in dict_keys([]) "
Let's add some lines of code to make this pass
- update the registration serializer in serializers.py with the code below
class RegistrationSerializer(serializers.ModelSerializer):
#new
password = serializers.CharField(max_length=40, min_length=8, write_only=True)
class Meta:
model = User
fields = ["email", "password"]
#new
def validate_password(self, value):
if value.isalpha() or value.isdigit():
raise serializers.ValidationError(
"password should be a mixed of letters and numbers"
)
return value
Run the test command again and we should have another passed test. Common celebrate we are almost done with the registration feature.
Now let's send in the right data with the right format, so what should we expect here
Our account table should be updated with new validated data
Success response should be returned
To test for the two, let's update the test class with the function below
from account.models import User
def test_api_register_valid_data(self) -> None:
data = {"email": "a@mail.com", "password": "azeez1233"}
response = self.client.post(self.reg_url, data=data)
db_users = User.objects.all()
self.assertIn("success", response.json().keys())
self.assertEqual(db_users.count(), 1)
self.assertEqual(response.status_code, 200)
of course the above test will fail if we run the test command. So let's fix that with just two lines of code inside the register function for our account.views file as shown below
@api_view(["POST"])
def register(request):
serializer = serializers.RegistrationSerializer(data = request.data)
if serializer.is_valid():
serializer.save() # new line
return Response({"success":"Registration successful"}) # new line
return Response(serializer.errors, status = status.HTTP_400_BAD_REQUEST)
Running the test again should show another passed test
So, going back to our list of features we want for our registration, we can see that we have added all the feature and we are sure of the working condition of the codes added.
Coverage report for the written test is as shown below
You can clone the final working code here
So, go ahead and write some tests for your added features and your team lead will be very happy to merge.
Till we meet again, HAPPY CODING!!!