A website cannot be perfect as it is nearly impossible for a website to run perfectly the first time without errors. As the project gets large it becomes hard for all the developers to keep track of all the changes; one single change in an existing component can make the whole project to break. This is why it is very essential to test every single functionality.
- Types Of Testing
- What Should Be Tested
- Our Testing Project
- Third Party Packages
- Django Package
Unit and Integration testing are two main types of testing.
- Unit Tests are isolated tests responsible for only one functionality.
- Integration Tests are aimed at mimicking the user behavior, usually combined with multiple functions to make sure they behave correctly.
As Unit Tests are small they should be written all the time. These tests are easy to debug. The more you write these, the lesser you will likely to write integration testing. However, sometimes it is essential to run integration testing.
- If something can break, it should be tested. These includes models, views, forms, templates, validators and so on.
- Each test should be focused on testing one functionality.
- It should run whenever a code is being PUSHed or PULLed from a repo in the staging environment, before PUSHing to production.
That being said if the code in question is a builtin django or python function/library, don't test it. For example datetime or model fields. The only time you test something if the function or code is written by you.
In this tutorial we will be using a very basic project book-library.
To set up the project, first we will need to clone the repo.
git clone https://github.com/rabbilyasar/book-library.git
Install virtualenv if not installed already
pip install virtualenv
activate our virtualenv
install the packages in our requirements.txt
pip install -r requirements.txt
asgiref==3.3.4 autopep8==1.5.7 coverage==5.5 Django==3.2.4 mccabe==0.6.1 pycodestyle==2.7.0 pytz==2021.1 sqlparse==0.4.1 toml==0.10.2
Migrate our database
python manage.py migrate
When you create a django app it includes a test.py file that you can use to write your tests, however, I prefer to create a module called tests and have separate files for our models, forms, views.
Note: If the project gets big it gets hard to maintain in this structure. Another way of doing would be to have a folder for models, views and forms and create file for each view or model.
We would be following the following structure for our project.
└── app_name └── tests ├── __init__.py ├── test_forms.py ├── test_models.py └── test_views.py
By default django's
unittest module uses its builtin-test-discovery. It will discover tests under the current directory with filename pattern test*.py. For our current structure to work we will have to delete our existing tests.py file which has been provided by our django app.
coverage: It can be used to have a rough overview of a project's total test coverage. It is a remarkable tool for any beginner as it can give you suggestion on what needs to be tested.
mock: This is a utilitarian tool for isolating part of your code that are not critical to the test you are writing.
model-bakery: This is a fine tool for creating fixtures for testing. We will go through some usage and examples in the next blog.
Let's a take a look at coverage and see how we can use it.
To run coverage inside a project:
pip install coverage
After every time you add some code to your application run this.
coverage run --omit='*/venv/*' manage.py test
If you want to see more details you can add -v flag. In this case it is using verbosity level 2.
coverage run manage.py test -v 2
After running it once we can can get a coverage report using:
We can also generate an HTML report in a new folder called htmlcov
Django's default framework for testing is Python's standard library module unittest. Despite the name this module can be used for both unit test and integration test. There are other modules like pytest, which has it's own benefits and drawbacks on which we will focus later.
Django's test client is an amazing tool which can be used for integration testing. It can simulate
POST requests on a
URL and one can observe the response. Test Client.
Django provides us with few base classes (SimpleTestCase, LiveServerTestCase, TransactionTestCase, TestCase). You can derive your test class from any of the base classes and have your own method which will test for a specific functionality.
class YourTestClass(TestCase): def setUp(self): # Setup run before every test method. pass def tearDown(self): # Clean up run after every test method. pass def test_something_that_will_pass(self): self.assertFalse(False) def test_something_that_will_fail(self): self.assertTrue(False)
The best base class for most tests is django.test.TestCase. This creates a clean database before its tests are run, and runs every test function in its own transaction. The test
client can be derived from this. TestCase provides us with an additional method
setUpTestData along with
setUp for setting up our data.
setUp(): It is called before every test method. This is useful when creating object that will need modifying before each test method.
setUpTestData(): This is called once before running the whole test. You use this one when you know the data you will create in this method will be the same for every test method. As this is run once for every test class, it is faster than
tearDown() method is not very useful when it comes to test that involves database. Moreover
TestCase does a full database flush at the start of a new test, so it will take care of the teardown for you.
We have some test methods provided to us by unittest, which can be used to test the condition of our code. Some standard assertions are:
AssertTrue: Checks if the condition is true.
AsserFalse: Checks if the condition is false.
AssertEqual: Checks if the condition is equal.
These are some of the python specific assert methods:
python unittest assert
Django specific assert methods:
django unittest assert
To run all the test in the project use the command:
python manage.py test
This will discover all files with the pattern test*.py under the directory and run all tests defined.
If we want to show more test information we can change the verbosity like below:
python manage.py test --verbosity 2
Note: Allowed verbosity levels are 0,1,2 and 3, the default being 1.
To run a specific test:
# Run the specified module python3 manage.py test app.tests # Run the specified module python3 manage.py test app.tests.test_models # Run the specified class python3 manage.py test app.tests.test_models.YourTestClass # Run the specified method python3 manage.py test app.tests.test_models.YourTestClass.your_test_method
In the next part of our series we will get our hands dirty and will see how we can implement unit testing to our book-library.