Ensuring Reliability: Test Cases in Django and Django Multi-Tenant

Test cases serve as a vital tool to ensure the quality and reliability of software during the development process. Test cases provide a structured approach to verify that the code functions as intended, handle various scenarios correctly and meets the specified requirements.

Developers rely on test cases to identify and rectify defects early on, reducing the likelihood of bugs reaching production and improving overall software quality and robustness.

Related read: Test Driven Development Approach using Django Rest Framework

I would like to highlight 6 crucial points that help developers from test cases.

6 crucial points that help developers from test cases

1. Early Detection of Defects: Test cases allow developers to catch defects and issues in their code early in the development cycle. By defining the test cases based on functional requirements, developers can systematically verify the behaviour of their code and quickly identify any discrepancies or errors.

2. Verification of Functionality: Test cases ensure that the implemented code performs its intended function accurately. Developers can design test cases to cover different use cases and scenarios, exercising various paths and inputs to validate the functionality of their code.

3. Code Refactoring and Maintenance: Test cases facilitate code refactoring and maintenance efforts. When making changes or updates to the codebase, developers can run relevant test cases to verify that the modifications have not introduced regressions or unintended side effects. This allows for more confident refactoring and ensures the stability of the system.

4. Regression Testing: As new features are added or existing functionality is modified, regression testing becomes crucial to ensure that previously working components are not negatively impacted. By having a comprehensive suite of test cases, developers can easily perform regression testing to catch any unintended consequences of code changes.

5. Debugging and Troubleshooting: Test cases provide developers with a structured approach to debugging and troubleshooting issues. When a test case fails, it serves as a starting point for investing and identifying the root cause of the problem. Developers can isolate and fix defects more efficiently by analyzing failing test cases and reproducing the issue.

6. Documentation and Code Understanding: Test cases serve as living documentation for the codebase. By examining test cases, developers gain insights into how different parts of the code should behave, which aids in understanding the system’s overall architecture and design.

Prerequisites

Before proceeding, it’s assumed that you have a working knowledge of the following technologies: Django and Rest API.

Additionally, having familiarity with the django_tenants library would be beneficial to fully grasp the concepts discussed in this blog.

Unit Test Cases in Django

In Django, unit test cases refer to the tests that focus on individual units of code, such as functions, methods or classes in isolation. These tests are designed to validate the behaviour and correctness of specific components of your Django application, typically at the smallest possible level.

Django provides a testing framework that includes the unittest module, allowing you to create test classes that inherit from Django.test.TestCase or unittest.TestCase these classes provide various utilities and assertions to simplify the process of writing unit tests.

In addition to regular unit tests for your code, Django also offers specialized test cases for testing Django-specific components, such as models, views, forms, URLs, middleware and template rendering. These specialized test cases provide additional utilities tailored to Django’s features and functionality.

Within the Django test classes, you define individual test methods that represent specific test cases. Each method should focus on testing a particular aspect or behaviour of the unit being tested. You can use assertion methods provided by the testing framework to validate expected results or conditions.

Unit test cases in Django you can ensure the individual components of your application are functioning correctly and independently of each other.

Below there is a folder structured that can write test cases in tests.py.

test-py-folder

TestCase Class

The TestCase class is widely used for writing tests in Django and is the recommended choice for most scenarios. It is a subclass of TransactionTestCase and SimpleTestCase making it suitable for applications that utilize a database. However, if your Django application does not require a database you can use the SimpleTestCase class instead.

setUp() method that allows you to define necessary code that should be executed before test cases run within the test class.

from django.test import TestCase





class DemoTestCase(TestCase):
"""
Class for writing test cases.
"""
def setUp(self):
"""
setUp method that sets data.
"""
Pass

This can include initializing variables, creating test data, configuring dependencies or preparing any resources required for testing. You can override the setUp() method in our test class and add your own custom setup code specific to your test cases.

Similarly, tearDown() executed after test cases will be executed. It allows you to clean up code that should be executed after each test case within a test class.

from django.test import TestCase





class DemoTestCase(TestCase):
"""
Class for writing test cases.
"""
def tearDown(self):
"""
setUp method that sets data.
"""
Pass

tearDown() method is automatically called by the testing framework after executing each individual test case within the test class. This ensures that the cleanup code is run consistently and independently for each test case. You can override the tearDown() method in your test class and add your own custom code specific to your test cases.

I have added a simple example below,

from django.test import TestCase
from .models import AuthorTable





class DemoTestCase(TestCase):
"""
Class for writing test cases.
"""
def setUp(self):
"""
setUp method that sets data.
"""
self.data = {
"email": "xyz.auther@yopmail.com",
"name": "xyz",
"number": "098764321"
}


def test_auther_model(self):
"""
Test method that tests author model.
"""
auther_object = AuthorTable.objects.create(**self.data)


self.assertEquals(auther_object.name, "xyz", msg="Name is correct!")

DemoTestCase class is defined and inherited from TestCase this will contain the test methods. setUp() method is overridden in the DemoTestCase class. This method is called before each test method and is used to set up any necessary data or resources for the test cases. In this example, it sets the self.data dict with some sample data.

test_auther_model() method is a test method within the DemoTestCase class. Test methods must start with the word test. It tests the AutherTable model by creating an instance of AuthorTable using the create() method with the self.data dict as keyword arguments. The self.assertEquals() assertion is used to verify that the name attribute of the author_object is equal to the expected value “xyz”. If the assertion fails an AssertionError is raised and the exception is printed.

To run the test cases, use the command python manage.py test in your terminal. After executing the command, you will see the test results displayed in your terminal, similar to the following snippets;

Found 1 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.
----------------------------------------------------------------------
Ran 1 test in 0.001s

OK
Destroying test database for alias 'default'...

The test runner provides detailed information about each test including any failures or errors encountered. It also provides coverage information if you have enabled test coverage analysis.

If the test cases fail, an assertion error is raised, and an exception is displayed in the terminal, as shown in the code snippet below;

Found 1 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
F
======================================================================
FAIL: test_auther_model (myapp.tests.DemoTestCase)
Test method that tests auther model.
----------------------------------------------------------------------
Traceback (most recent call last):
File "/home/mindbowser/Django Practice Local/test_cases_demo/myapp/tests.py", line 25, in test_auther_model
self.assertEquals(auther_object.name, "xyzx", msg="Name is correct!")
AssertionError: 'xyz' != 'xyzx'
- xyz
+ xyzx
? +
: Name is correct!

----------------------------------------------------------------------
Ran 1 test in 0.002s

FAILED (failures=1)
Destroying test database for alias 'default'...

You can find more information about the assertion methods available in Django’s testing framework in the official Django documentation on testing and assertion methods.

Learn the Best Practices of React Unit Testing with Jest – Watch Our Comprehensive Video Now!

Test Cases in Multi-Tenant

In a multi-tenant project where multiple clients or tenants share the same application infrastructure writing effective test cases is crucial to ensure the stability and reliability of the system. Test cases in a multi-tenant project focus on verifying the functionality of data isolation and security aspects specific to the multi-tenancy architecture.

I have built a multi-tenant platform using the django_tenant library. If you don’t know about this library, I suggest going through the documentation a little bit to understand the test cases in a multi-tenant platform.

Talking about test cases with the django_tenants, the library provides a test class which is SubfolderTestCase and TestCase imported from django_tenants.test.cases widely used to write test cases.

SubfolderTestCase Class

The SubfolderTestCase class is a test case provided by the django_tenants library which is an extension for Django that enables multi-tenancy support. This test case is specifically designed for testing applications using the subfolder-based multi-tenancy approach.

The subfolder-based multi-tenant approach involves different tenants accessing the application using a unique subfolder in the URL.

class SubfolderTenantTestCase(TenantTestCase):
"""Adds a public tenant to support tests against TenantSubfolderMiddleware
"""


@classmethod
def setUpClass(cls):
# Set up public tenant
cls.public_tenant = get_tenant_model()(schema_name=get_public_schema_name())
cls.public_tenant.save()


super().setUpClass()


@classmethod
def tearDownClass(cls):
super().tearDownClass()
cls.public_tenant.delete()

The SubfolderTenantTestCase class shown in the code is a custom test case class that extends the TenantTestCase provided by the django_tenants library. This class is designed to support testing against the TenantSubfolderMiddleware which is used in subfolder-based multi-tenant applications.

The SubfolderTenantTestCase class is defined as inheriting from the TenantTestCase class. It is specifically created to add a public tenant that can be used for testing purposes against the TenantSubfolderMiddleware in a subfolder-based multi-tenant application.

The setUpClass() method is overridden to set up a public tenant before running the test cases. It creates a new instance of the tenant model using the get_tenant_model() and the get_public_schema_name() functions. The public tenant is then saved to the database. After setting up the public tenant the super().setUpClass() method is called to perform any additional setup defined in the parent class.

The tearDownClass() method is overridden to perform cleanup after all the test cases have been executed. It calls the super().tearDownClass() method to clean up any resource defined in the parent class and then deletes the public tenant from the database.

I have written test cases in Django and shared some test cases below;

class SetUpTestData(SubfolderTenantTestCase, TestCase):
"""
Class to create dummy test data.
"""


@classmethod
def get_test_tenant_domain(cls):
return "localhost"


@classmethod
def get_test_schema_name(cls):
return "test"

The setUpTestData class shown in the code is a custom test case class that extends the SubfolderTenantTestCase. It is designed to create dummy test data specifically for testing purposes.

The get_test_tenant_domain() method is overridden to specify the domain name to be used for the test tenant. In this case, it returns localhost indicating that the test should be associated with the localhost domain.

The get_test_schema_name() method is overridden to specify the schema name to be for the test tenant. In the above example, it returns a test indicating that the test tenant should be associated with the test schema.

By overriding these two methods the SetUpTestData class provides custom configurations for the test tenant’s domain and schema name.

class LoginTestCase(SetUpTestData):
"""
Class for testing user login.
"""


def setUp(self):
self.super_user = self.setUpInitial()
self.passed = "PASSED"
self.failed = "FAILED"
self.path = base_test_url + reverse(test_url["login"])
self.applicationJson = "application/json"


user, user_profile, role = self.setUpOrgAdmin()


self.data = {"email": user.email, "password": "Qwerty@123", "role": "USER", "platform": "web"}


super().setUp()
self.c = TenantClient(self.tenant)


def test_user_login(self):
response_data = self.c.post(self.path, data=self.data, content_type=self.applicationJson)
status_code = response_data.data["status_code"]
try:
self.assertEqual(status_code, status.HTTP_200_OK)
print_test.print_terminal(self.path, "LOGIN", self.passed, None)
except AssertionError as e:
print_test.print_terminal(self.path, "LOGIN", self.failed, e)

The LoginTestCase class shown in the code is a custom test case class that extends the SetUpTestData class. It is designed to test the user login functionality created in the setUp method by the setUpOrgAdmin().

Also, here overridden the setUp() method to specify the necessary data previously which is needed while the test method runs.

Once data and necessary attributes are defined, also define the TenantClient with the specific tenant. TenantClient is a custom class used for making HTTP requests to a Django application in the context of a specific tenant in a multi-tenant environment. It is typically used in test cases or other scenarios where you need to interact with the application as a specific tenant.

In multi-tenant architectures, each tenant has its own isolated environment, including its own database schema or subdomain. The TenantClient class helps you make HTTP requests to the application while specifying the context of a particular tenant, allowing you to test or interact with tenant-specific functionality.

Here the test case method is test_user_login() which makes a request with the specific tenant that we are already defined in the setUp method and also carries a payload with the valid credentials. It then checks the status code of the response and uses self.assertEqual() to assert that the status code is status.HTTP_200_OK indicates a successful login.

Creating test database for alias 'default'...
System check identified no issues (0 silenced).
----------------------------------------------------------------------------------------------------
1 API : /api/localhost/users/login || TEST CASE : LOGIN || STATUS : PASSED || MESSAGE : None
.
----------------------------------------------------------------------
Ran 1 test in 17.842s

OK
Destroying test database for alias 'default'...
CSV FILE CREATED SUCCESSFULLY

After running the test cases above response has been generated. You might be wondering if the response is a little bit different from the previous response. This response is customized with the help of the print() function. We created a class for this type of response and also generated a CSV file of test case results we talk about later on this functionality.

Generate CSV File

When you write test cases and run successfully you need to share the test case results with the QA to analyze the result and make sure functionality is intact and performs well. Hence if the Django test result file is not created we need to track each test case and store it, after all test cases run successfully then we are created the test file in CSV format.

class PrintTestCases:
def __init__(self):
self.count = 0
self.header = ["API", "TEST CASE", "STATUS", "MESSAGE"]
self.data = list()


def create_test_case_csv(self):
open_file = open("testcases.csv", "w")
writer = csv.writer(open_file)
writer.writerow(self.header)
writer.writerows(self.data)
print("CSV FILE CREATED SUCCESSFULLY")
return


def print_terminal(self, api_name, test_case, test_status, msg):
self.data.append([api_name, test_case, test_status, msg])
self.count = self.count + 1
print("-" * 100)
print(f"{self.count} API : ", api_name, "|| TEST CASE : ", test_case, "|| STATUS :", test_status, "|| MESSAGE : ", msg)





print_test = PrintTestCases()

In the above code snippets, we created a class for printing the test result in the terminal and also create test result files.

A PrintTestCases class is used for managing and printing test case results. The PrintTestCases class has an __init__ method that initializes instance variables. It initializes self.count to keep track of the number of test cases, self.header as a list representing the header row of a CSV file, and self.data as an empty list to store the test case data.

The create_test_case_csv() method is responsible for creating a CSV file and writing the test case data to it. It opens a file named “testcases.csv” in write mode, creates a csv.writer object, writes the header row using writer.writerow(self.header) and then writes the test case data using writer.writerows(self.data). Finally, it prints a message indicating that the CSV file has been created.

The print_terminal() method is used to print test case details to the terminal. It appends a new row of test case data ([api_name, test_case, test_status, msg]) to the self.data list. It increments self.count to keep track of the number of test cases. Then it prints a horizontal line (“-” * 100) and prints the test case details using formatted strings.

Finally, an instance of the PrintTestCase class is created as print_test. So we can use this instance to call the methods defined within the class such as create_test_case_csv() or print_terminal(), you can see the use of this instance in below test case code snippets;

def test_user_login(self):
response_data = self.c.post(self.path, data=self.data, content_type=self.applicationJson)
status_code = response_data.data["status_code"]
try:
self.assertEqual(status_code, status.HTTP_200_OK)
print_test.print_terminal(self.path, "LOGIN", self.passed, None)
except AssertionError as e:
print_test.print_terminal(self.path, "LOGIN", self.failed, e)

To create a CSV file of the test case results, you can use the following code snippets;

from django.test.runner import DiscoverRunner
from utilities.tests import print_test





class CustomTestRunner(DiscoverRunner):
def run_tests(self, test_labels, extra_tests=None, **kwargs):
result = super().run_tests(test_labels, extra_tests=extra_tests, **kwargs)


print_test.create_test_case_csv()
return result

In the above code snippet, we build our custom test runner using DiscoverRunner to fully fill our requirements. Basically, I defined CustomTestRunner to generate test result CSV files with the help of the create_test_case_csv() method. After creating this class just defined it in the settings.py file as shown below.

TEST_RUNNER = "test_runner.CustomTestRunner"

Finally after running the test cases results will be displayed one by one of each test case that you write and after all test cases have been executed CSV file generated with the test case results. Below I have shared my terminal snippets;

Creating test database for alias 'default'...
System check identified no issues (0 silenced).
----------------------------------------------------------------------------------------------------
1 API : /api/localhost/users/addRole?platform=web || TEST CASE : ADD ROLE || STATUS : PASSED || MESSAGE : None
.----------------------------------------------------------------------------------------------------
2 API : /api/localhost/users/addUser?platform=web || TEST CASE : ADD USER || STATUS : PASSED || MESSAGE : None
.----------------------------------------------------------------------------------------------------
3 API : /api/localhost/users/changePassword?platform=web || TEST CASE : CHANGE PASSWORD || STATUS : PASSED || MESSAGE : None
.----------------------------------------------------------------------------------------------------
4 API : /api/localhost/users/deleteUser/2/?platform=web || TEST CASE : DELETE USER || STATUS : PASSED || MESSAGE : None
.----------------------------------------------------------------------------------------------------
5 API : /api/localhost/users/updateUser/2/?platform=web || TEST CASE : EDIT PROFILE || STATUS : PASSED || MESSAGE : None
.----------------------------------------------------------------------------------------------------
6 API : /api/localhost/users/forgetPassword?platform=web || TEST CASE : FORGOT PASSWORD REQUEST || STATUS : PASSED || MESSAGE : None
.----------------------------------------------------------------------------------------------------
7 API : /api/localhost/users/getUserProfile || TEST CASE : GET PROFILE || STATUS : PASSED || MESSAGE : None
.----------------------------------------------------------------------------------------------------
8 API : /api/localhost/users/getUserProfile || TEST CASE : GET PROFILE FILTERS || STATUS : PASSED || MESSAGE : None
.----------------------------------------------------------------------------------------------------
9 API : /api/localhost/users/listManagerNames?platform=web || TEST CASE : MANAGER NAMES LIST || STATUS : PASSED || MESSAGE : None
.----------------------------------------------------------------------------------------------------
10 API : /api/localhost/users/listOwners || TEST CASE : LIST OWNERS || STATUS : PASSED || MESSAGE : None
.----------------------------------------------------------------------------------------------------
11 API : /api/localhost/users/listOwners || TEST CASE : LIST OWNERS FILTERS || STATUS : PASSED || MESSAGE : None
.----------------------------------------------------------------------------------------------------
12 API : /api/localhost/users/listRoles?platform=web || TEST CASE : LIST ROLES || STATUS : PASSED || MESSAGE : None
.----------------------------------------------------------------------------------------------------
13 API : /api/localhost/users/listUsers || TEST CASE : LIST USERS || STATUS : PASSED || MESSAGE : None
.----------------------------------------------------------------------------------------------------
14 API : /api/localhost/users/listUsers || TEST CASE : LIST USERS FILTERS || STATUS : PASSED || MESSAGE : None
.----------------------------------------------------------------------------------------------------
15 API : /api/localhost/users/login || TEST CASE : LOGIN || STATUS : PASSED || MESSAGE : None
.----------------------------------------------------------------------------------------------------
16 API : /api/localhost/users/resetPassword?platform=web || TEST CASE : RESET PASSWORD || STATUS : PASSED || MESSAGE : None
.----------------------------------------------------------------------------------------------------
17 API : /api/localhost/users/updateRole/2/?platform=web || TEST CASE : UPDATE ROLE || STATUS : PASSED || MESSAGE : None
.----------------------------------------------------------------------------------------------------
18 API : /api/localhost/users/updateUser/2/?platform=web || TEST CASE : UPDATE USER || STATUS : PASSED || MESSAGE : None
.----------------------------------------------------------------------------------------------------
19 API : /api/localhost/users/verifyOtp?platform=web || TEST CASE : VERIFY OTP || STATUS : PASSED || MESSAGE : None
.
----------------------------------------------------------------------
Ran 19 tests in 148.347s

OK
Destroying test database for alias 'default'...
CSV FILE CREATED SUCCESSFULLY

At the end, you can read the message CSV file created successfully. The CSV file is created and shared with you below;

CSV-file-report

coma

Conclusion

Test cases play a crucial role in ensuring the quality and reliability of your web applications. They allow you to verify that your code functions correctly, identify and fix bugs and provide a safety net when making changes or adding new features. Django provides a comprehensive testing framework that includes various tools and classes to facilitate the creation and execution of test cases.

So when working with multi-tenant projects in Django, test cases become even more important. Multi-tenancy introduces additional complexities related to isolating and testing functionally specific to individual tenants. Specialized test case classes such as TenantTestCase and SubfolderTestCase from Django packages like django_tenants. These classes provide mechanisms for handling tenant-specific tenant-specific configurations, schema or subdomains during testing ensuring that tenant-specific functionality is thoroughly tested.

Test cases in a multi-tenant project should cover scenarios that validate the behavior of the application across different tenants ensuring that tenant isolation is maintained and that tenant-specific features and customizations work as expected.

Keep Reading

Keep Reading

Struggling with EHR integration? Learn about next-gen solutions in our upcoming webinar on Mar 6, at 11 AM EST.

Register Now

Let's create something together!