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.
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.
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.
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.
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.
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.
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.
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;
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.
How to Effectively Hire and Manage a Remote Team of Developers.
Download NowThe Mindbowser team's professionalism consistently impressed me. Their commitment to quality shone through in every aspect of the project. They truly went the extra mile, ensuring they understood our needs perfectly and were always willing to invest the time to...
CTO, New Day Therapeutics
I collaborated with Mindbowser for several years on a complex SaaS platform project. They took over a partially completed project and successfully transformed it into a fully functional and robust platform. Throughout the entire process, the quality of their work...
President, E.B. Carlson
Mindbowser and team are professional, talented and very responsive. They got us through a challenging situation with our IOT product successfully. They will be our go to dev team going forward.
Founder, Cascada
Amazing team to work with. Very responsive and very skilled in both front and backend engineering. Looking forward to our next project together.
Co-Founder, Emerge
The team is great to work with. Very professional, on task, and efficient.
Founder, PeriopMD
I can not express enough how pleased we are with the whole team. From the first call and meeting, they took our vision and ran with it. Communication was easy and everyone was flexible to our schedule. I’m excited to...
Founder, Seeke
Mindbowser has truly been foundational in my journey from concept to design and onto that final launch phase.
CEO, KickSnap
We had very close go live timeline and Mindbowser team got us live a month before.
CEO, BuyNow WorldWide
If you want a team of great developers, I recommend them for the next project.
Founder, Teach Reach
Mindbowser built both iOS and Android apps for Mindworks, that have stood the test of time. 5 years later they still function quite beautifully. Their team always met their objectives and I'm very happy with the end result. Thank you!
Founder, Mindworks
Mindbowser has delivered a much better quality product than our previous tech vendors. Our product is stable and passed Well Architected Framework Review from AWS.
CEO, PurpleAnt
I am happy to share that we got USD 10k in cloud credits courtesy of our friends at Mindbowser. Thank you Pravin and Ayush, this means a lot to us.
CTO, Shortlist
Mindbowser is one of the reasons that our app is successful. These guys have been a great team.
Founder & CEO, MangoMirror
Kudos for all your hard work and diligence on the Telehealth platform project. You made it possible.
CEO, ThriveHealth
Mindbowser helped us build an awesome iOS app to bring balance to people’s lives.
CEO, SMILINGMIND
They were a very responsive team! Extremely easy to communicate and work with!
Founder & CEO, TotTech
We’ve had very little-to-no hiccups at all—it’s been a really pleasurable experience.
Co-Founder, TEAM8s
Mindbowser was very helpful with explaining the development process and started quickly on the project.
Executive Director of Product Development, Innovation Lab
The greatest benefit we got from Mindbowser is the expertise. Their team has developed apps in all different industries with all types of social proofs.
Co-Founder, Vesica
Mindbowser is professional, efficient and thorough.
Consultant, XPRIZE
Very committed, they create beautiful apps and are very benevolent. They have brilliant Ideas.
Founder, S.T.A.R.S of Wellness
Mindbowser was great; they listened to us a lot and helped us hone in on the actual idea of the app. They had put together fantastic wireframes for us.
Co-Founder, Flat Earth
Ayush was responsive and paired me with the best team member possible, to complete my complex vision and project. Could not be happier.
Founder, Child Life On Call
The team from Mindbowser stayed on task, asked the right questions, and completed the required tasks in a timely fashion! Strong work team!
CEO, SDOH2Health LLC
Mindbowser was easy to work with and hit the ground running, immediately feeling like part of our team.
CEO, Stealth Startup
Mindbowser was an excellent partner in developing my fitness app. They were patient, attentive, & understood my business needs. The end product exceeded my expectations. Thrilled to share it globally.
Owner, Phalanx
Mindbowser's expertise in tech, process & mobile development made them our choice for our app. The team was dedicated to the process & delivered high-quality features on time. They also gave valuable industry advice. Highly recommend them for app development...
Co-Founder, Fox&Fork