You must have written numerous functions and a series of tasks while developing software or a web app. These functions and tasks must work properly. If we encounter errors in the code, debugging becomes difficult.
A good practice would be to divide our code into small units or parts and test them independently to ensure that they work properly.
Python provides a built-in module called unittest
that allows us to write and run unit tests.
Getting Started With unittest
The unittest
module includes a number of methods and classes for creating and running test cases. Let's look at a simple example where we used the unittest
module to create a test case.
# basic.py
import unittest
class TestSample(unittest.TestCase):
def test_equal(self):
self.assertEqual(round(3.155), 3.0)
def test_search(self):
self.assertIn("G", "Geek")
First, we imported the unittest
module, which will enable us to use the classes that will be used to write and execute test cases.
The TestSample
class is defined that inherits from the unittest.TestCase
which will allow us to use the various assertion methods within our test cases.
We defined two test methods within our TestSample
class: test_equal
and test_search
.
The test method test_equal()
tests if round(3.155)
is equal to 3.0
using the assertEqual()
assertion method.
The test method test_search()
tests if the character "G"
is present in the string "Geek"
using the assertIn()
assertion method.
To run these tests, we need to execute the following command in the terminal.
python -m unittest basic.py
This command will launch unittest
as a module that searches for and executes the tests in the basic.py
file.
Note: The unittest
module only discovers and executes those methods that start with test_
or test
.
By the way, these dots represent a successful test.
We can use the unittest.main()
function and put it in the following form at the end of the test script to load and run the tests from the module.
if __name__ == '__main__':
unittest.main()
This will allow us to run our test file, basic.py
in this case, as the main module.
More Detailed Result
We can use the -v
flag in the terminal or pass an argument verbosity=2
inside the unittest.main()
function to get a detailed output of the test.
Commonly Used Assertion Methods
Here is the list of the most commonly used assertion methods in unit testing.
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) |
Example
Assume we have some code and want to perform unit testing on it using the unittest
module.
# triangle.py
class Triangle:
def __init__(self, base, height):
self.base = base
self.height = height
def area(self):
return 0.5 * self.base * self.height
def perimeter(self, side):
return self.base + self.height + side
The code defines a class called Triangle
which has an init method that initializes the object with the instance variables self.base
and self.height
.
There are two more methods in the Triangle
class: area()
and perimeter()
.
The area()
method returns the area of the triangle, which is half the product of the base and height (0.5 * self.base * self.height
).
The method parameter()
accepts a parameter called side
, and because the triangle's parameter is the sum of its three sides, the base
and height
variables take the place of the other two sides.
Now we can create another Python file in which we'll write some tests and then execute them.
# test_sample.py
from triangle import Triangle
import unittest
class TestTriangle(unittest.TestCase):
t = Triangle(9, 8)
def test_area(self):
self.assertEqual(self.t.area(), 36)
def test_perimeter(self):
self.assertEqual(self.t.perimeter(5), 22)
def test_valid_base(self):
self.assertGreater(self.t.base, 0)
def test_valid_height(self):
self.assertGreater(self.t.height, 0)
if __name__ == '__main__':
unittest.main(verbosity=2)
The above code imports the Triangle
class from the triangle
module (triangle.py
file) as well as imports the unittest
module to write test cases.
The TestTriangle
class inherits from the unittest.TestCase
which has four test methods. The Triangle
class was instantiated with a base
of 9
and height
of 8
and stored inside the variable t
.
The test_area
method tests whether self.t.area()
is equal to the expected result 36
using the assertEqual()
assertion.
The test_perimeter
method tests whether self.t.perimeter(5)
is equal to 22
using the assertEqual()
assertion.
The test_valid_base
and test_valid_height
methods are defined to test if the base (self.t.base
) and height (self.t.height
) of the triangle are greater than 0
using the assertGreater()
assertion.
The unittest.main(verbosity=2)
method retrieves and executes the tests from the TestTriangle
class. We'll get a detailed output because we used the verbosity=2
argument.
Test for Exception
If you've used assert statements before, you'll know that when one fails, it throws an AssertionError
. Similarly, whenever a test method fails, an AssertionError
is raised.
We can predetermine the conditions under which our code will generate an error, and then test those conditions to see if they generate errors. This is possible with the assertRaises()
method.
The assertRaises()
method can be used with context manager so we'll use it in the following form:
def test_method(self):
with assertRaises(exception_name):
function_name(argument)
Consider the following function gen_odd()
, which generates a series of odd numbers up to the argument n
by incrementing the num
by 3
and contains only a few checks, where the argument n
must be of type int
and greater than 0
.
# odd.py
def gen_odd(n):
if type(n) != int:
raise TypeError("Invalid argument type.")
if n < 0:
raise ValueError("Value must be greater than 0.")
num = 0
while num <= n:
if num % 2 == 1:
print(num)
num += 3
Now we'll write test methods to simulate conditions that could cause the above code to fail.
from odd import gen_odd
import unittest
class OddTestCase(unittest.TestCase):
def test_negative_val(self):
with self.assertRaises(ValueError):
gen_odd(-5)
def test_float_val(self):
with self.assertRaises(TypeError):
gen_odd(99.9)
def test_string_val(self):
with self.assertRaises(TypeError):
gen_odd('10')
if __name__ == '__main__':
unittest.main(verbosity=2)
We wrote three test methods in the OddTestCase
class to ensure that when invalid arguments are passed, the corresponding error is raised.
The test_negative_val()
method asserts that ValueError
is raised when gen_odd(-5)
is called.
Similarly, the test_float_val()
and test_string_val()
methods assert that when gen_odd(99.9)
and gen_odd('10')
are called, respectively, TypeError
is raised.
All three tests in the above code passed, which means they all raised corresponding errors, otherwise, the tests would have failed or raised the errors if another exception was raised. Let's put it to the test.
from odd import gen_odd
import unittest
class OddTestCase(unittest.TestCase):
def test_valid_arg(self):
with self.assertRaises(TypeError, msg="Valid argument"):
gen_odd(10)
if __name__ == '__main__':
unittest.main(verbosity=2)
The above condition within the test_valid_arg()
method will not throw a TypeError
because gen_odd()
function is passed with a valid argument.
The above test method failed and raised an AssertionError
with the message TypeError not raised : Valid argument
.
Skipping Tests
The unittest
makes use of the skip()
decorator or skipTest()
to skip any test method or whole test class on purpose, and we are required to specify the reason why the test is being skipped.
Consider the previous example's code, which we modified by adding the skip()
decorator.
from odd import gen_odd
import unittest
class OddTestCase(unittest.TestCase):
@unittest.skip("Valid argument")
def test_valid_arg(self):
with self.assertRaises(TypeError):
gen_odd(10)
if __name__ == '__main__':
unittest.main(verbosity=2)
It's clear that the above condition will fail and throw an AssertionError
, so we skipped the testing on purpose.
What if we wanted to skip the test if a particular condition was true? We can accomplish this by using the skipIf()
decorator, which allows us to specify a condition and skip the test if it is true.
from odd import gen_odd
import sys
import unittest
class OddTestCase(unittest.TestCase):
@unittest.skipIf(sys.getsizeof(gen_odd(10)) > 10, "Exceeded limit")
def test_memory_use(self):
self.assertTrue(sys.getsizeof(gen_odd(10)) > 10)
print(f"Size: {sys.getsizeof(gen_odd(10))} bytes")
if __name__ == '__main__':
unittest.main(verbosity=2)
The condition in the above skipIf()
decorator checks whether the size of gen_odd(10)
is greater than 10
bytes, if the condition is true, the test method test_memory_use()
is skipped, otherwise, the test is executed.
Expected Failure
If we have a test method or test class with conditions that are expected to be false, we can use the expectedFailure()
decorator to mark them as expected failures instead of checking for errors.
from odd import gen_odd
import sys
import unittest
class OddTestCase(unittest.TestCase):
@unittest.expectedFailure
def test_memory_use(self):
self.assertTrue(sys.getsizeof(gen_odd(10)) < 10, msg="Expected to be failed")
print(f"Size: {sys.getsizeof(gen_odd(10))} bytes")
if __name__ == '__main__':
unittest.main(verbosity=2)
We've modified the previous code and the condition we are checking inside the test_memory_use()
method is expected to be false, which is why the method is decorated with the @unittest.expectedFailure
decorator.
Conclusion
We can use the unittest
module to write and run tests to ensure that the code is working properly. The test can result in one of three outcomes: OK, FAIL, or Error.
The unittest
module provides several assertion methods that are used to validate the code.
Let's recall, what we've learned:
the basic usage of
unittest
module.CLI commands to run the tests.
testing if the condition is raising an exception.
skipping the tests on purpose and when a certain condition is true.
marking a test as an expected failure.
πOther articles you might be interested in if you liked this one
β How to use assert statements for debugging in Python?
β What are the uses of asterisk(*) in Python?
β What are __init__ and __new__ methods in Python?
β How to implement getitem, setitem and delitem in Python classes?
β How to change the string representation of the object using str and repr methods?
β How to generate temporary files and directories using tempfile in Python?
β Build a custom deep learning model using the transfer learning technique.
That's all for now
Keep codingββ
Top comments (0)