As software developers, we all write lots and lots of lines of code while building an application. However, to ensure that each and every component works perfectly in the software, we really need to do some unit testing. This ensures proper functionality and reliable product performance. This testing of individual components is known as Unit Testing.
For the dynamic nature and ease of writing tests alongside the code, Python can be a viable option for unit testing of our software. So, let's dive into the nitty-gritty of writing unit tests and explore the best practices and techniques to ensure our code's reliability and maintainability.
Often, it's found that during the early development phase, unit tests serve as a safety net by helping us catch bugs and regressions. By verifying the behavior of individual units, one can always able to identify and fix issues before they propagate throughout the codebase.
Also, well-written unit tests act as documentation for the code, providing examples of its expected behavior. When making changes or refactoring code, we can easily rely on the existing tests to ensure that modifications don't inadvertently break functionality.
In Python, we generally write unit tests using testing frameworks such as unittest
. These tests validate specific behaviors of individual units, typically by asserting expected outcomes against actual results. The unittest
module is included in Python's standard library, which provides a framework for organizing and running unit tests and offers its own classes and methods for creating the test cases, running them, and reporting the results.
For this case, we are considering a simple Flask application that uses MongoDB as the database. Now, we will be writing some APIs to interact with the database, and we'll be testing them by writing unit test cases for them using the unittest
library!!
Let's create the file app.py
. Here, we will connect our app with the database and write our API !!
First, let's import our necessary packages and connect the MongoDB container named task_manager
with our application:
from flask import Flask, request, jsonify
from pymongo import MongoClient
from flask_cors import CORS
from bson import ObjectId
app = Flask(__name__)
cors = CORS(app, resources={r"/api/*": {"origins": "*"}})
client = MongoClient('mongodb://localhost:27017/task_manager')
db = client['task_manager']
collection = db['tasks']
Now, with the database being connected, let's write one API for the application:
@app.route('/api/tasks', methods=['POST'])
def create_task():
data = request.get_json()
task = {
'title': data['title'],
'description': data['description']
}
result = collection.insert_one(task)
return jsonify({'message': 'Task created successfully', 'id': str(result.inserted_id)}), 201
With our app ready, now let's write the unit test case for this API we have just written!!
Let's create another file called test_app.py
where we'll write the test case for our application. Now, let's first import the necessary libraries required for the testing purpose:
import unittest
from app import app
from unittest.mock import patch, MagicMock
import json
from bson import ObjectId
Now, let's create our testing class and do the setup along with writing the test case for our POST
request API:
class TestTaskManager(unittest.TestCase):
# Unit test case for testing the Task Manager API.
def setUp(self):
"""
Set up the test client and mock the collection used in the app.
This method runs before each test.
"""
# Initialize the test client for the app
self.app = app.test_client()
self.app.testing = True
# Create a mock for the collection used in the app
self.collection_mock = MagicMock()
# Patch the collection in the app with the mock
self.patcher = patch('app.collection', self.collection_mock)
self.patcher.start()
def tearDown(self):
"""
Stop the patcher after each test.
This method runs after each test.
"""
# Stop the patcher to clean up after tests
self.patcher.stop()
def test_create_task(self):
"""
Test the task creation endpoint.
"""
# Data to be sent in the POST request
data = {'title': 'Test Task', 'description': 'This is a test task'}
# Make a POST request to create a new task
response = self.app.post('/api/tasks', json=data)
# Assert that the response status code is 201 (Created)
self.assertEqual(response.status_code, 201)
# Assert that the response contains the success message
self.assertIn(b'Task created successfully', response.data)
Now that our test case has been written, it's time to put it on some test. So, we need to run the command python3 test_app.py
in the terminal to get the result of the test,
It looks kind of like this!! As we can see, our test was passed successfully.
In case of any failure, we will get the result something like this when the command is run in the terminal:
This could happen if there is any problem with the data that is being sent if there are some flaws, or if there is any problem with the address.
Now, if we had multiple APIs in our application, we would have to write more test cases to test each one of them!!
Now, what if we want to check how much code our written unit test cases cover!? Here comes Keploy, which helps us to easily check our test coverage in some simple steps.
First, we need to install Keploy's Python SDK:
pip install keploy pytest
Next, we can create a test file for running Keploy's API tests, and we can name the file test_keploy.py
and the contents of the file will be as follows:
from keploy import run, RunOptions
def test_keploy():
try:
options = RunOptions(delay=15, debug=False, port=0)
except ValueError as e:
print(e)
run("python3 -m coverage run -p --data-file=.coverage.keploy python3 app.py", options)
We also need to create a .coveragerc
file to ignore the coverage of the libraries that is calculated. The contents of the file will be as follows:
[run]
omit =
/usr/*
sigterm = true
Now, to run our unit test with Keploy, we can run the command given below:
python3 -m coverage run -p --data-file=.coverage.unit -m pytest -s test_keploy.py test_app.py
Now, to combine the coverage from the unit tests and Keploy's API tests and then generate the coverage report for the test run, we can use these commands:
python3 -m coverage combine
python3 -m coverage report
The practices mentioned below are not exclusive to Python but work for all kinds of unit test cases. But as we are discussing unit test case writing here, it's important to mention it here:
Keep Tests Simple and Focused
While writing a unit test, we must focus on a single aspect of functionality, keeping the test cases as simple and easy to understand as possible.
Use Descriptive Test Names
Clear and descriptive test names improve the readability and help other developers/maintainers understand the purpose of each test case.
Isolate Test Cases
We should avoid dependencies between test cases by isolating the units under test. And for that, we can use techniques such as mocking or dependency injection to replace external dependencies with test doubles.
Now that we know about the best practices for writing unit test cases let's focus on some common mistakes that we must avoid while writing them. These include:
Testing Implementation Details
Always avoid testing the implementation details because, as far as I've seen, these tests can become prone to breaking when refactoring the code.
Ignoring Edge Cases
Remember to ensure that our unit tests cover edge cases and boundary conditions to validate the robustness of our code under various problematic scenarios.
Writing unit tests is a fundamental aspect of software development, which ensures code reliability and maintainability. By following best practices, leveraging advanced techniques, and also embracing tools like Keploy, developers can create robust test suites that validate their code's behavior under different conditions. And well, that's a wrap for now!! Hope you folks have enriched yourself today with lots of known or unknown concepts. I wish you a great day ahead, and till then, keep learning and keep exploring!!
What is the purpose of unit testing?
Unit testing aims to validate the individual units or components of a software application to ensure that they behave as expected, enhancing code reliability and maintainability.
How do I write my first unit test in Python?
To write your first unit test in Python, you'll need to set up a test environment, create a test case class that inherits from unittest.TestCase
, write test methods within the class to validate specific behaviors, and then run your tests using a test runner.
Which tools and libraries are available for unit testing in Python?
Python offers several tools and libraries for unit testing, including pytest, which simplifies test writing and execution, and nose2, an extension of Python's built-in unit test framework with additional features for test discovery and running.