-
Notifications
You must be signed in to change notification settings - Fork 0
TESTING IN PYTHON
Exploratory testing is a form of manual testing that is done without a plan. In an exploratory test, you’re just exploring the application.
To have a complete set of manual tests, all you need to do is make a list of all the features your application has, the different types of input it can accept, and the expected results. Now, every time you make a change to your code, you need to go through every single item on that list and check it.
Automated testing is the execution of your test plan (the parts of your application you want to test, the order in which you want to test them, and the expected responses) by a script instead of a human.
Python already comes with a set of tools and libraries to help you create automated tests for your application.
Testing multiple components is known as integration testing.
An integration test checks that components in your application operate with each other.
A major challenge with integration testing is when an integration test doesn’t give the right result. It’s very hard to diagnose the issue without being able to isolate which part of the system is failing.
A unit test is a smaller test, one that checks that a single component operates in the right way. A unit test helps you to isolate what is broken in your application and fix it faster.
Eample:
To write a unit test for the built-in function sum(), you would check the output of sum() against a known output.
>>> assert sum([1, 2, 3]) == 7, "Should be 6"
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AssertionError: Should be 6
If the result from sum() is incorrect, this will fail with an AssertionError and the message "Should be 6".
If the result is correct, nothing will be printed on the screen.
Instead of testing on the REPL, you’ll want to put this into a new Python file called test_sum.py and execute it again:
def test_sum():
assert sum([1, 2, 3]) == 6, "Should be 6"
if __name__ == "__main__":
test_sum()
print("Everything passed")
Writing tests in this way is okay for a simple check, but what if more than one fails? This is where test runners come in. The test runner is a special application designed for running tests, checking the output, and giving you tools for debugging and diagnosing tests and applications.
There are many test runners available for Python. The one built into the Python standard library is called unittest.
The three most popular test runners are:
- unittest
- nose or nose2
- pytest
contains both a testing framework and a test runner.
unittest requires that:
- You put your tests into classes as methods
- You use a series of special assertion methods in the
unittest.TestCaseclass instead of the built-inassertstatement
To convert the earlier example to a unittest test case, you would have to:
- Import
unittestfrom the standard library - Create a class called
TestSumthat inherits from theTestCaseclass - Convert the test functions into methods by adding self as the first argument
- Change the assertions to use the
self.assertEqual()method on theTestCaseclass - Change the command-line entry point to call
unittest.main()
import unittest
class TestSum(unittest.TestCase):
def test_sum(self):
self.assertEqual(sum([1, 2, 3]), 6, "Should be 6")
def test_sum_tuple(self):
self.assertEqual(sum((1, 2, 2)), 6, "Should be 6")
if __name__ == '__main__':
unittest.main()
python test_sum_unittest.py
.F
======================================================================
FAIL: test_sum_tuple (__main__.TestSum)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_sum_unittest.py", line 9, in test_sum_tuple
self.assertEqual(sum((1, 2, 2)), 6, "Should be 6")
AssertionError: Should be 6
----------------------------------------------------------------------
Ran 2 tests in 0.001s
FAILED (failures=1)
You may find that over time, as you write hundreds or even thousands of tests for your application, it becomes increasingly hard to understand and use the output from unittest.
nose is compatible with any tests written using the unittest framework and can be used as a drop-in replacement for the unittest test runner.
The development of nose as an open-source application fell behind, and a fork called nose2 was created. If you’re starting from scratch, it is recommended that you use nose2 instead of nose.
To get started with nose2, install nose2 from PyPI and execute it on the command line. nose2 will try to discover all test scripts named test*.py and test cases inheriting from unittest.TestCase in your current directory:
$ pip install nose2
$ python -m nose2
.F
======================================================================
FAIL: test_sum_tuple (__main__.TestSum)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_sum_unittest.py", line 9, in test_sum_tuple
self.assertEqual(sum((1, 2, 2)), 6, "Should be 6")
AssertionError: Should be 6
----------------------------------------------------------------------
Ran 2 tests in 0.001s
FAILED (failures=1)
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:
- Support for the built-in assert statement instead of using special
self.assert*()methods - Support for filtering for test cases
- Ability to rerun from the last failing test
- An ecosystem of hundreds of plugins to extend the functionality
Writing the TestSum test case example for pytest would look like this:
def test_sum():
assert sum([1, 2, 3]) == 6, "Should be 6"
def test_sum_tuple():
assert sum((1, 2, 2)) == 6, "Should be 6"
You have dropped the TestCase, any use of classes, and the command-line entry point.
Pytest Documentation Website.
project/
│
├── my_sum/
│ └── __init__.py
|
└── tests/
└── test.py
Creating the __init__.py file means that the my_sum folder can be imported as a module from the parent directory.
The test file will need to be able to import your application to be able to test it, you want to place test.py above the package folder
my_sum/__init__.py
def sum(arg):
total = 0
for val in arg:
total += val
return total
Create a folder called tests/ and split the tests into multiple files. It is convention to ensure each file starts with test_ so all test runners will assume that Python file contains tests to be executed.
You can import any attributes of the script, such as classes, functions, and variables by using the built-in __import__() function. Instead of from my_sum import sum, you can write the following:
target = __import__("my_sum.py")
sum = target.sum
The benefit of using __import__() is that you don’t have to turn your project folder into a package, and you can specify the file name. This is also useful if your filename collides with any standard library packages. For example, math.py would collide with the math module.
- Create your inputs
- Execute the code being tested, capturing the output
- Compare the output with an expected result
test.py
import unittest
from my_sum import sum
class TestSum(unittest.TestCase):
def test_list_int(self):
"""
Test that it can sum a list of integers
"""
data = [1, 2, 3]
result = sum(data)
self.assertEqual(result, 6)
if __name__ == '__main__':
unittest.main()
This code example:
- Imports
sum()from themy_sumpackage you created - Defines a new test case class called
TestSum, which inherits fromunittest.TestCase - Defines a test method,
test_list_int(), to test a list of integers. The methodtest_list_int()will:- Declare a variable
datawith a list of numbers(1, 2, 3) - Assign the result of
my_sum.sum(data)to aresultvariable - Assert that the value of
resultequals 6 by using theassertEqual()method on theunittest.TestCaseclass
- Declare a variable
- Defines a command-line entry point, which runs the
unittesttest-runnermain()
The last step of writing a test is to validate the output against a known response. This is known as an assertion.
There are some general best practices around how to write assertions:
- Make sure tests are repeatable and run your test multiple times to make sure it gives the same result every time
- Try and assert results that relate to your input data, such as
checking that the result is the actual sum of values in the sum()
example
unittestcomes with lots of methods to assert on the values, types, and existence of variables. Here are some of the most commonly used methods:
| Method | Equivalent to |
|---|---|
assertEqual(a, b) |
a == b |
assertTrue(x) |
bool(x) is True |
assertFalse(x) |
bool(x) is False |
assertIs(a, b) |
assertIs(a, b) |
assertIsNone(x) |
x is None |
assertIn(a, b) |
a in b |
assertIsInstance(a, b) |
isinstance(a, b) |
assertIs(), assertIsNone(), assertIn(), and assertIsInstance() all have opposite methods, named assertIsNot(), and so forth.
When you’re writing tests, it’s often not as simple as looking at the return value of a function. Often, executing a piece of code will alter other things in the environment, such as the attribute of a class, a file on the filesystem, or a value in a database. These are known as side effects and are an important part of testing.
If you find that the unit of code you want to test has lots of side effects, you might be breaking the Single Responsibility Principle.
This means the piece of code is doing too many things and would be better off being refactored.
The Python application that executes your test code, checks the assertions, and gives you test results in your console is called the test runner.
At the bottom of test.py, you added this small snippet of code:
if __name__ == '__main__':
unittest.main()
This is a command line entry point. It means that if you execute the script alone by running python test.py at the command line, it will call unittest.main(). This executes the test runner by discovering all classes in this file that inherit from unittest.TestCase.
Another way is using the unittest command line. Try this:
$ python -m unittest test
This will execute the same test module (called test) via the command line.
You can provide additional options to change the output. One of those is -v for verbose.
$ python -m unittest -v test
test_list_int (test.TestSum) ... ok
----------------------------------------------------------------------
Ran 1 tests in 0.000s
Instead of providing the name of a module containing tests, you can request an auto-discovery using the following:
python -m unittest discover
This will search the current directory for any files named test*.py and attempt to test them.
Once you have multiple test files, as long as you follow the test*.py naming pattern, you can provide the name of the directory instead by using the -s flag and the name of the directory:
python -m unittest discover -s tests
unittest will run all tests in a single test plan and give you the results.
Lastly, if your source code is not in the directory root and contained in a subdirectory, for example in a folder called src/, you can tell unittest where to execute the tests so that it can import the modules correctly with the -t flag:
python -m unittest discover -s tests -t src
unittest will change to the src/ directory, scan for all test*.py files inside the the tests directory, and execute them.