Unit testing is a software testing method by which individual units of source code are put under various tests to determine whether they are fit for use (Source). It basically determines and ascertains the quality of your code.
Tests can be Unit Tests, Integration Tests, Load Tests, Stress Tests, and so on. The tests are important to ensure the stability of the system in different scenarios and input conditions.
Unit Testing is unarguably among the most important skills that every developer should have. Any large application with thousands or millions of moving parts cannot be tested for regressions manually.
Here at LinuxAPT, as part of our Server Management Services, we regularly help our Customers perform related Python unit testing queries.
In this context, you shall learn about Unit Testing which is a fully automated way of testing the smallest piece of code also known as a unit.
The main goal here is to break the code on local machines rather than in production.
Unit Testing differs from traditional manual testing in a few ways:
1. It is fully automated. The test cases written are coded and are run automatically or manually. In work, one may notice that the build or deployment pipelines run the tests before merging and/or deploying your code.
2. Tests the smallest unit of code. Often the smallest unit means a method or a function, but it can also be a single line of code.
3. It allows "test-driven development", an agile development strategy to write tests before any amount of code is done and check if the tests pass after development is done.
1. Improves the quality of code
Writing tests forces us to create functional components which can be easily tested. This practice makes code modular and maintainable.
2. Detects software bugs in early stages
No code is without bugs and a good developer should know which condition can result in a bug. Unit tests can detect scenarios that can be considered rare while manually testing a software, however, in Live such scenarios may occur more often.
3. Edge case scenarios are tested beforehand
Conditions like Integer Overflow, IndexOutOfBound, or even NullPointers can be identified well before the code is deployed in live environments. We should also consider that the code is supposed to throw errors at certain scenarios and unit tests are a good way to test if those errors are being actually thrown.
4. Reduces both cost and development time
Writing extensive unit tests does take a considerable amount of time, however, they pay off, in the long run, considering the benefits they provide. Bugs not caught early in the development stage are skipped to testing and production to spend more time in debugging, bug fixing, and deployment.
5. Simplify the documentation
Unit tests act as documentation on their own for the developer's code. Every class or file for which unit tests are being written covers all different scenarios and reading the unit tests should give enough insight into what the component is trying to achieve.
6. Fortified Deployment and Code Submit Pipelines
Deployment pipelines can be set up to run unit tests before a system is being deployed and even well before deployment, while we submit our code to a version control system (Git, for example). This ensures that the deployments shall not fail and any new feature being added shall not break what is already working.
Python comes with the unittest package, originally inspired by JUnit.
However, we don’t need to install any packages.
There are certain important concepts which unittest supports like in Junit.
1. test fixture
If one is coming from a Java and Junit background they must be familiar with setUp() and tearDown() used in unit tests. In python, test fixtures do the same thing. They can be used to create temporary directories, database connections, fakes, mocks etc in setUp() and destroy those objects and temp directories and connections in tearDown().
2. test case
A test case is analogous to a class we create for testing a functionality. It checks for the responses returned for a combination of inputs.
3. test suite
While running tests for a code, one may want to group multiple tests under a single umbrella, so that they run together. We can group test cases and/or test suites.
4. test runner
The test runner does the magic of orchestrating the execution of tests. Later in the article you may notice, the outcome rendered can be in textual format and/or a graphical interface.
most commonly used assert methods provided by the TestCase class to check and report for failures:
Method - Checks That
assertEqual(a, b) - a == b
assertNotEqual(a, b) - a != b
assertTrue(x) - bool(x) is True
assertFalse(x) - bool(x) is False
assertIs(a, b) - a is b
assertIsNot(a, b) - a is not b
assertIsNone(x) - x is None
assertIsNotNone(x) - x is not None
assertIn(a, b) - a in b
assertNotIn(a, b) - a not in b
assertIsInstance(a, b) - isinstance(a, b)
assertNotIsInstance(a, b) - not isinstance(a, b)
Here, try to implement a few for testing Calculator.py:
class Calculator:
def add(self, a: int, b: int) -> int:
return a + b
def sub(self, a: int, b: int) -> int:
return a - b
def mul(self, a: int, b: int) -> int:
return a * b
def div(self, a: int, b: int) -> float:
return a / b
Calculator_test.py tests our calculator logic.
Notice we consider negative scenarios as well:
import unittest
from Calculator import Calculator
class CalculatorTest(unittest.TestCase):
def setUp(self) -> None:
super().setUp()
self.calculator = Calculator()
def test_add(self):
# Check add(5, 6) gives 11 or not
self.assertEqual(11, self.calculator.add(5, 6),
"Testing Calculator.add()")
def test_sub(self):
# Check sub(5, 6) gives -1 or not
self.assertEqual(-1, self.calculator.sub(5, 6),
"Testing Calculator.sub()")
def test_sub_fail(self):
# This test case will fail as sub(5, 6) is -1 not 10
self.assertEqual(10, self.calculator.sub(5, 6), "Testing Calculator.sub()")
def test_mul(self):
# Run single test multiple times with different parameters
# Parameter Source
params = [
{
'a': 5,
'b': 6,
'ret': 30
},
{
'a': 5,
'b': -10,
'ret': -50
},
{
'a': 0,
'b': 6,
'ret': 0
},
{
'a': 9,
'b': -6,
'ret': -54
},
]
For param in params:
self.assertEqual(param['ret'], self.calculator.mul(
param['a'], param['b']), "Testing Calculator.mul()")
def test_div(self):
# Check div(40, 2) gives 20 or not
self.assertEqual(20, self.calculator.div(
40, 2), "Testing Calculator.div()")
# Check div(20, 40) gives 0.5 or not
self.assertEqual(20/40, self.calculator.div(20, 40),
"Testing Calculator.div()")
# Check div(10, 3) gives 3.3333333 or not (rounded upto 7 decimal places)
self.assertAlmostEqual(3.3333333, self.calculator.div(10, 3),
msg="Testing Calculator.div()")
# Check whether Divide by 0 throws exception ZeroDivisionError
self.assertRaises(ZeroDivisionError, self.calculator.div, 20, 0)
if __name__ == "__main__":
unittest.main()
A test case inherits unittest.TestCase. setUp() prepares the test fixture and will run before executing each test method. The name of individual test methods needs to start with test_ to be recognized by the test runner.
subTest() context manager is used to distinguish test iterations inside the body of a test method.
But if you are using IDE like Pycharm or VSCode then there is a GUI test runner, which shows success and failures graphically.
In VSCode open the test file, press Shift + P and select Python: Run Current Test File
You should see failed and passed test cases
There are few best practices we should follow while writing unit tests:
1. Arrange, Act and Assert
i. Arrange: Every test we write, first we arrange our data for the test.
ii. Act: In this stage we act on the method or component we intend to test
iii. Assert: Here we assert the output we receive from our component.
Ensure that there is a line space after each stage of our test and code for every stage can be grouped together.
2. Before and After
Make sure the common code which is required for each and every test should be added in test fixtures. Use setUp() for running logic before every test and tearDown() for logic to be run after tests.
3. Fakes Vs Mocks
Always try to avoid mocking wherever possible and use the actual components themselves. However, if this cannot be avoided, try to use fakes, where we can fake a component, service or some I/O operation. Mocking should be the last resort taken while testing since mocking makes a lot of assumptions which may or may not hold true in real scenarios.
When test methods involve RPC (REST/GraphQL/gRPC) or database queries, we mock the wire calls. But that’s out of the scope of this article and will be covered in future articles.
4. Meaningful Names
Make sure your tests have meaningful names and the reader can guess what is happening just by reading the name of the test.
This article covers the concept of Unit Testing in Python. Testing in Python is a huge topic and can come with a lot of complexity, but it doesn't need to be hard. You can get started creating simple tests for your application in a few easy steps and then build on it from there.
Here, You'll learn about the tools available to write and execute tests, check your application's performance, and even look for security issues.
pytest supports execution of unittest test cases. The real advantage of pytest comes by writing pytest test cases. pytest test cases are a series of functions in a Python file starting with the name test_.
pytest has some other great features:
1. Support for the built-in assert statement instead of using special self.assert*() methods
2. Support for filtering for test cases
3. Ability to rerun from the last failing test
4. An ecosystem of hundreds of plugins to extend the functionality
Running Your Tests From Visual Studio Code
If you're using the Microsoft Visual Studio Code IDE, support for unittest, nose, and pytest execution is built into the Python plugin.
If you have the Python plugin installed, you can set up the configuration of your tests by opening the Command Palette with Ctrl+Shift+P and typing "Python test".
How to Use unittest and Flask
Flask requires that the app be imported and then set in test mode. You can instantiate a test client and use the test client to make requests to any routes in your application.
All of the test client instantiation is done in the setUp method of your test case. In the following example, my_app is the name of the application. Don’t worry if you don’t know what setUp does. You’ll learn about that in the More Advanced Testing Scenarios section.
The code within your test file should look like this:
import my_app
import unittest
class MyTestCase(unittest.TestCase):
def setUp(self):
my_app.app.testing = True
self.app = my_app.app.test_client()
def test_home(self):
result = self.app.get('/')
# Make your assertions
You can then execute the test cases using the python -m unittest discover command.