Testing
Contents
Testing#
BB1000 Programming in Python#
KTH
layout: false
Learning goals#
As a result of taking this module you will be able to
Know main reasons for testing
Develop code with a test-driven development appraoch
Use one of Python’s main libraries for testing (pytest)
Know how to prepare tests with fixtures
Know how to measure how code is covered by tests
Why testing?#
Make sure the code is correct.
Testing is a design tool
Testing is a documentation tool
It makes for a better design
Code will never be written that is difficult to test
Code tends to get better structure
It’s what professionals do
Because it saves time and money
What is a test?#
All testing is based on the assert
statement
>>> assert ...
The
assert
commandIf
...
is true, do nothingIf
...
is false, stop the programCan be used to check stuff in production code, e.g. consistent input
Different types of tests
unit test: testing of small units of code (programmers view)
integration test: testing how units work together (clients view)
Same tools are used for both cases
What is a test in practice?#
An single function that uses assert
to compare actual vs expected result
>>> def test_slash_join():
... assert slash_join_strings('abc', 'def') == 'abc/def'
>>> def slash_join_strings(s1, s2):
... return "/".join((s1, s2))
Several test functions may be collected in a file
A test suite is a collection of test files.
A test runner executes all tests it can find
What is test-driven development (TDD)?#
To add new functionality, most often involves writing a function
Make up in your mind what the function should do
What input is required
What output is expected
However, do not code the function just yet…
In brief
Write a test which contains some
assert
statementWrite code such the test can pass
The TDD work cycle#
Testing a function
Initialize#
Write a test that
calls the function
assert
that the actual output and expected output are the same
Repeat#
Run the test
Did it fail?
The first time: yes
Add code to the function with the purpose of passing the test
Start over
Did it pass?
Stop
Do not write more code. You are done.
Tools#
Tools for testing python code#
external
pytest: a commonly used third-party tool for running tests
nose: same but older
builtin
unittest: a module of the standard python library to provide advanced testing
doctest: a simple way of including tests in a doc-string of a function
Pytest#
Understands many styles of tests
The most modern testing framework
Easy to get started
Advanced features
It supports test coverage
It couples to the python debugger
Example#
Consider
#my_math.py
def test_my_add():
assert my_add(1, 1) == 2
def my_add(x, y):
return x - y
Execute the file with pytest
Collects everything that looks like a test
Executes them one by one
#my_math.py
def test_my_add():
assert my_add(1, 1) == 2
def my_add(x, y):
return x - y
```shell $ pytest my_math.py ============================= test session starts ============================== platform linux -- Python 3.6.3, pytest-3.5.0, py-1.5.3, pluggy-0.6.0 rootdir: /home/olav, inifile: pytest.ini collected 1 item
my_math.py F [100%]
=================================== FAILURES =================================== _________________________________ test_my_add __________________________________
def test_my_add():
assert my_add(1, 1) == 2
E assert 0 == 2 E + where 0 = my_add(1, 1)
my_math.py:3: AssertionError =========================== 1 failed in 0.02 seconds ===========================
---
```python
#my_math.py
def test_my_add():
assert my_add(1, 1) == 2
def my_add(x, y):
return x + y
```shell $ pytest my_math.py ============================= test session starts ============================== platform linux -- Python 3.6.3, pytest-3.5.0, py-1.5.3, pluggy-0.6.0 rootdir: /home/olav, inifile: pytest.ini collected 1 item
my_math.py . [100%]
=========================== 1 passed in 0.00 seconds ===========================
---
## Exercise
def test_leap_year(): … assert is_leap_year(2000) == True … assert is_leap_year(1999) == False … assert is_leap_year(1998) == False … assert is_leap_year(1996) == True … assert is_leap_year(1900) == False … assert is_leap_year(1800) == False … assert is_leap_year(1600) == True
* Save in a file `test_leap.py`
* Write code in `leap.py`
* Verify with pytest
---
## Test coverage
* Coverage is a measure how much of your code is executed by tests
* During a test run a coverage library keeps track of executed lines
* Guides developer where bugs may hide
How it works (pytest)
* Summary
C:\Users…> pytest test_my.py –cov my
* Line info
C:\Users…> pytest test_my.py –cov my –cov-report=term-missing
---
* HTML report
C:\Users…> pytest test_my.py –cov my –cov-report=html
Open in browser
<img src="cov1.png" height="250">
<img src="cov2.png" height="250">
---
## Doctest
* Simple test cases can be provided as part of the documentation string
* Cut and paste an interactive session known to give true result
* The doctest module executes the example from the interactive session as a test case
* Test on string output and not values - small changes in formatting will case tests to fail
---
## Example
#calculate.py
def add(a, b):
"""Return sum of two arguments
>>> add(1, 1) #doctest: +SKIP
2
>>>
"""
return a + b
def sub(a, b):
"""Return difference of two arguments
>>> sub(1, 1) #doctest: +SKIP
0
"""
return a + b
can you see the bug?
---
## Running doctest
On the command line
$ python -m doctest calculate.py
* All code in the file is executed
* Functions are defined
* Test are run
---
### The output
$ python calculate.py
**********************************************************************
File "calculate.py", line 14, in __main__.sub
Failed example:
sub(1, 1)
Expected:
0
Got:
2
**********************************************************************
1 items had failures:
1 of 1 in __main__.sub
***Test Failed*** 1 failures.
---
Correct the bug
return a + b -> return a - b
Rerun
$ python -m doctest calculate.py
$
silent - all ok
---
## Conclusion - doctests
* Very easy to include testning into your code
* The test serves as documentation as well
* Typically tests only one aspect of the function
* But could clutter your code and may not be the best for extensive testing
* Extensive testing is best separated from production code
* More information on http://docs.python.org/library/doctest
---
## The unittest module
* A unit test module exist for this purpose: `unittest`
* The tests can be written in a separate file
* One defines a class which is a subclass of `unittest.TestCase`
* The unittest framework executes and checks everything that begins with test
* Part of the standard library and provides very portable testing
---
### Howto
* Define a class with a name beginning with ``Test`` as a subclass of ``unittest.TestCase``
* Define class methods that begin with ``test`` using the test functions of the ``unittest`` module
* Optionally one may define a ``setUp`` and a ``tearDown`` method which are run before and after every test.
* In the main section run ``unittest.main()``
class TestSomething(unittest.TestCase):
...
def test_this(self):
...
def test_that(self):
...
if __name__ == "__main__":
unittest.main()
---
### Example
```python
#test_calculate.py
import unittest
import calculate
class TestCalculate(unittest.TestCase):
def testadd(self):
res = calculate.add(1, 1)
self.assertEqual(res, 2)
def testsub(self):
res = calculate.sub(1, 1)
self.assertEqual(res, 0)
if __name__ == "__main__":
unittest.main()
compare with
#my_math.py
def test_add():
assert calculate.add(1, 1) == 1
def test_sub():
assert calculate.sub(1, 1) == 0
Run test#
$ python test_calculate.py
.F
======================================================================
FAIL: testsub (__main__.TestCalculate)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_calculate.py", line 18, in testsub
self.assertEqual(res, 0)
AssertionError: 2 != 0
----------------------------------------------------------------------
Ran 2 tests in 0.001s
FAILED (failures=1)
Run verbose test#
$ python test_calculate.py -v
testadd (__main__.TestCalculate) ... ok
testsub (__main__.TestCalculate) ... FAIL
======================================================================
FAIL: testsub (__main__.TestCalculate)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_calculate.py", line 18, in testsub
self.assertEqual(res, 0)
AssertionError: 2 != 0
----------------------------------------------------------------------
Ran 2 tests in 0.001s
FAILED (failures=1)
Fix the bug#
return a + b -> return a - b
–
Rerun test#
$ python test_calculate.py -v
testadd (__main__.TestCalculate) ... ok
testsub (__main__.TestCalculate) ... ok
----------------------------------------------------------------------
Ran 2 tests in 0.000s
OK
Other helper functions tests#
assertNotEqual
assertTrue
assertFalse
assertAlmostEqual
Most numerical testing is within a threshold, e.g.
def testdiv(self):
res = calculate.div(1., 3)
self.assertAlmostEqual(res, 0.333333, 6)
see also http://docs.python.org/3/library/unittest.html
Summary#
The
pytest
library is the most modern testing framework for PythonEasy to get started with simple test functions
nose
is a similar older libraryThe standard library contains
unittest
libraryRequires knowledge of classes to code your tests,
Fine to
doctest
for small illustrations
Final tip#
Embrace the TDD philosphy, write test before code.
Document code and modules - be kind to your future self.
For good programming style, consider PEP 8, http://www.python.org/dev/peps/pep-0008/
Be obsessive about testing
If your test code is larger that your production code, you are on the right track
This takes initially a little more time but the rewards in the long run are huge
Links#
http://pythontesting.net
http://mathieu.agopian.info/presentations/2015_06_djangocon_europe
http://katyhuff.github.io/python-testing
Amazon search:”TDD” > 100 books
http://pyvideo.org: About 80 results