There are two types of developers in the programming world. The first one writes code and walks away, does not watch it run. They assume everything will go according to plan. There is the second type, who is writing unit tests. With Python built-in test modules, you can effectively write code to test your software's modules. It will give you peace of mind that eludes all too many programmers. Most companies and teams require their code to be thoroughly examined. This saves you a lot of time and also gives you confidence that your updates and refactoring don't have any unintended consequences to break your code execution in a way.
So, in this article, I will cover the details of the basic understanding of unit testing, its importance, the key principles and benefits while writing code in Python, how to write tests, its environment setup, and some best practices.
Understanding Unit Testing
Definition of Unit Testing
Unit testing is the practice or software testing framework used for examining the smallest piece of code, referred to as a unit, and testing them individually to check if they are functioning as intended. Additionally, it isolates the smallest piece of testable software from the remainder of the code to determine if it behaves as expected and according to the requirement in operation.
Purpose of Unit Testing
The primary purpose of unit testing is to verify each unit or component of the software individually and whether each functionality is working based on the user's requirements. It encourages the developer to write cleaner and modular code that is independent of other modules. Using unit testing, you can easily maintain the software application over time.
Key Principles of Unit Testing
Unit testing is the initial phase of software testing, where an individual unit or component of the software is tested against the needs and requirements of the user. Several key principles help us to ensure the correct testing mechanism so that each unit performs the expected designed functionality. Here are the principles:
1. Isolation
The first fundamental testing principle is isolation, which means that each unit that has undergone testing should not be independent of other modules. Each functionality is tested separately, like the module having one specific functionality. It ensures that the issue in one module does not affect the working of another module. So, make sure each tested unit is isolated.
2. Independence
Independence is another principle that requires consideration while testing. It means that each unit test is independent of the other unit test. In this way, the test failure of one unit does not impact the execution of another unit. Also, it ensures that the unit testing is done in any order and that one module testing is not waiting for another unit result.
3. Repeatable
This principle is most widely used for automated test suites, where results should be consistent irrespective of how many times we run the unit test. Therefore, the unit test code should be repeatable.
4. Fast
Each unit test should be able to quickly identify the defect when the testing fails. This principle ensures the fastest way of testing the individual unit instead of testing the whole application at once.
Benefits of Unit Testing
1. Early Bug Detection
Unit testing helps us find software bugs early. It is carried out by developers who test individual code prior to integrating multiple modules. Bugs or issues are identified very early and resolved without impacting other pieces of code, thus facilitating early detection of errors in the software.
2. Simplified Debugging
Unit testing also makes debugging easier and quicker. It helps simplify the debugging process, if a test fails only then the latest changes made in the code need to be debugged.
3. Code Documentation
Code documentation mostly shows how little code documentation gets written. Unit testing can make the documentation burden a little easier by encouraging better coding practices and leaving behind the piece of code that describes what your program is going to achieve.
4. Refactoring Confidence
It makes the coding process more agile when you add more and more features to software applications and sometimes need to change the old design and code. However, changing the already-tested code is both risky and extremely costly. If we have a unit test in place, then we can proceed with refactoring very confidently to verify each part of a code is working properly.
Getting Started with Unit Testing in Python
1. Introduction to Python's Built-in Unit Testing Framework: unittest
Python has introduced two main modules for performing unit testing, i.e. unittest and pytest. The unitest module is part of the Python standard library. So, there is no need to pip install it. It is a testing framework that allows us to write and execute unit tests for our Python modules. It has a set of classes and methods that provide us the functionality to write unit tests and execute them, working based on object-oriented concepts.
2. Setting up a project for Unit Testing
Before starting the technical details to understand the unit testing, make sure you installed Python with the VS Code on your machine and also set up the project in the necessary directory. So, let's get started.
2-1. Project Structure
First, create a directory and name it according to your choice. Then, create two subdirectories in it; the first one is named src, used for saving the main source code without unit testing. The second one is named test, where you put the unit test code. So, this is the way to set up the project in Python using the necessary directory structure. Let's say we have the following project structure:
![][1]
2-2. Installation of necessary Libraries
As discussed above, unittest is the testing framework and is part of the Python standard library. So, there is no need for additional installation. Also, just make sure that the python is installed on your computer.
Unit Testing Algorithm
![][2]
Let us see a simple algorithm of unit testing in which you can start by writing test code and adding these tests to the test suit. Then you run the unit test, if the test passes, you add one more test or add to the next level of testing. In case of test failure, you make the necessary changes and execute the unit test again but failed. However, you repeat the cycle and make changes until the code is perfectly working and returns the expected results. On the other hand, after making changes if the test passes, you can add it to the test suite and move on to the next unit test. After completing all the tests and checking the functionality you can stop the testing.
3. Writing your First Unit Test
3-1. Creating Source Code
Before demonstrating the testing, you require the source code upon which unit testing is performed. Suppose you are making a simple calculator that performs basic math operations like addition, subtraction, multiplication, and division. All these operations are written in the file named calculator.py, which is placed in the src directory. Let's move towards the coding part.
print("SIMPLE CALCULATOR")
print("Having operation '+', '-', '*' and '/'")
class calculator:
def addition(num1, num2): #function for addition
num3 = num1 + num2
return num3
def subtraction(num1, num2): #function for subtraction
num3 = num1 - num2
return num3
def multiplication(num1, num2): #function for multiplication
return num1 * num2
def division(num1, num2): #function for division
num3 = num1 / num2
return num3
result1 = addition(10, 5)
result2 = multiplication(5, 5)
print("Addition of two numbers:" , result1)
print("Multiplication of two numbers:" , result2)
3-2. Writing Test Methods
Now, we want to test the methods inside the above code. For that, we need to import the unittest module, then required to create a test method which is based on the base class TestCase containing functions for performing unit testing.
import unittest #importing the module
from src_code.calculation import calculator # importing the function form the main class
class Testing(unittest.TestCase): #used the base class for creating testing subclass
def test_addition(self): #create the test function
self.assertEqual(calculator.addition(5,5), 10) #takes the values and its answer and check its correctness
self.assertEqual(calculator.addition(6,7), 13)
def test_multiplication(self): #another function
self.assertEqual(calculator.multiplication(5,5), 25) #takes the values and its answer and check its correctness
self.assertEqual(calculator.multiplication(4,2),8)
if __name__ == '__main__': #used to class the main class of unittest
unittest.main()
In the above code, addition() and multiplication() functions of calculator.py are tested. To achieve this, we have to do the following:
- Import the unittest library and the calculator class from calculator.py.
- Then, create the testing method for both addition() and multiplication() methods using the calculator instance.
- Use the assertEqual function that checks that the given values output or the expected output should be the same and equal.
3-3. Running Tests
For running the unit test, we use unittest.main(), which will execute the test when the program runs. It will displays the output when the program tested successfully but, if the program fails, the compiler will highlight the error in the terminal and show that the testing failed.
![][3]
If we change the excepted output values of the multiplication() function, then testing fails and this will be displayed in the terminal:
![][4]
Remember, it is the convention that all the testing methods should start with the keyword test_ because this way unittest can easily identify the method that needs to be executed.
Anatomy of a Unit Test
1. Test fixture setup and teardown
After understanding the basics of the unittest module, we move towards the more advanced concepts of the testing framework used for optimizing the code. Test fixture setup and teardown are widely used for initializing and cleaning up the task before and after the test execution. Suppose we create a class called employee having all employee details. Now, we want to test the emails and payment increment of each employee, for that, we have to write the test code for each employee, and it's hectic. Also, it creates redundant code repeatedly. So, to resolve this problem, I am going to use two components in the unittest modules used for optimizing the code.
1-1. setUp() method
The setUp() method will run its code before every single test is executed. It is helpful when you have to perform the same task again and again in each test function. Therefore, placing that piece of code in the setUp() function will help you in redundancy reduction and optimization. Let's move towards the coding part for better understanding.
employee.py
class Employee: #employee class
increment_amt = 1.05 #variable with increment value
def __init__(self, name, pay): #function for taking values
self.name = name
self.pay = pay
@property
def email(self): #email function
return '{}@gmail.com'.format(self.name)
def increment(self): #function of incremeting the payment
self.pay = int(self.pay * self.increment_amt)
emp_testing.py
import unittest #importing the module
from src_code.employee import Employee
class Testing(unittest.TestCase): #used the base class for creating testing subclass
def setUp(self): #setup the resource before each execution
self.emp1 = Employee('john', 50000)
self.emp2 = Employee('stephen', 60000)
def test_email(self):
self.assertEqual(self.emp1.email, '*Emails are not allowed*')
self.assertEqual(self.emp2.email, '*Emails are not allowed*')
def test_increment(self):
self.emp1.increment()
self.emp2.increment()
self.assertEqual(self.emp1.pay, 52500)
self.assertEqual(self.emp2.pay, 63000)
if __name__ == '__main__': #used to class the main class of unittest
unittest.main()
![][5]
1-2. tearDown() method
The tearDown() will run its code after the test execution. It is for releasing the resources after using it like database connection or some cleanup activities.
def setUp(self): #setup the resource before each execution
self.emp1 = Employee('john', 50000)
self.emp2 = Employee('stephen', 60000)
def tearDown(self): #deleting the employee
del self.emp1
2. Writing Assertions
2-1. Using Assert Statements
In Python, the assert is the built-in statement used for checking the conditions. If the condition is true, it will not show anything but in case of error or the false condition, it will raise the error. Most assert statements are considered as exceptions and errors, but developers prefer their utilization for testing and debugging purposes.
n = 1
assert 1 < n, 'This condition is false' #checks and print only if its false
Output
![][6]
2-2. Common Assert Methods
As we discussed the assert statement above, there is no need to use it separately in the program. The unittest module contains the base class TesCase and provides all the assert functions that work exactly like assert statements without detailed coding. These methods are used within the test method to check whether the resulting output matches the excepted output. Here, I will discuss the common assert methods provided by the TestCase class of unittest.
![][7]
Always try to avoid testing the external dependencies like databases, files, or network connections because they slow down tests and make them brittle.
Q: What is the unittest.TestCase in the unittest module?
A: unittest.TestCase is the base class of the unittest module that serves as a container for all individual test
Q: When should I start writing the test cases?
A: The best approach is to write when you are developing the application because it ensures that application is free of bugs from the early stage.
Wrapping Up
To sum up, we discussed the basics of unit testing and got to know its importance in the development and maintenance of software. We also learn the unit testing techniques by using the coding examples. It shows that unit testing is a very crucial phase before other testing. It also ensures that the individual module is error-prone and modular. Developer needs to understand that the use of unit testing during the application developement will save most of their time and ensure the quality of software. I hope this guide is helpful for you. Also, if you have any questions or concerns, then feel free to ask and give feedback. Thank you for following this guide. Happy Coding!
Reference
Unit Testing
[1]: https://logiclair.org/?qa=blob&qa_blobid=16541390965964414061
[2]: https://logiclair.org/?qa=blob&qa_blobid=4202928536661986523
[3]: https://logiclair.org/?qa=blob&qa_blobid=7841557182954355083
[4]: https://logiclair.org/?qa=blob&qa_blobid=9280295931013992206
[5]: https://logiclair.org/?qa=blob&qa_blobid=1643797813287049192
[6]: https://logiclair.org/?qa=blob&qa_blobid=3774790010506432385
[7]: https://logiclair.org/?qa=blob&qa_blobid=12721698954172678740