2.18. lib - pytest

The concept of writing code to test code can be a bit bizarre at first, but the idea is to test the behavior of a function or integration of functions with a set of inputs that have a known desired output value (ie. add 1 + 3; the inputs are 1 and 3, and we know that the correct answer should be 4).

Why write tests:

  • Code reliability. Does the code work the way it is was intended?

  • Code maintainability. During refactoring or feature addition/removal does the code still do what it was supposed to do?

  • Code longevity. During version updates, outputs of dependent packages can change. A properly tested code will ensure that all outputs of dependent packages still work for your code.

  • Tested code will yield cleaner code as well. It will be come very apparent in practice that a single function that does everything will be much hard to test (and read) than that same code refactored into many smaller functions with explicit output.

Assertions: Python.org

2.18.1. Setup your first test

Although it is not necessary to create tests in another file, it is highly encouraged to keep the code base nice and clean.

  • The code we want to test: let’s say the the following is in a file mycode.py

# filename: mycode.py

def adder(x,y):
    return x + y
  • The test code: let’s say the following is in a file test_mycode.py It is a good idea to keep write a “test” file for each “code” file. Code files should also be short (not 1000s of lines).

# filename: test_mycode.py

# import the builtin test library
import unittest
# import your code module (mycode.py, assuming both mycode.py and test_myode.py is in the same folder)
import mycode


# setup the test class
class test_mycode(unittest.TestCase):

    # setUp is optional: but if you are re-using inputs this saves you from retying them
    def setUp(self):
        self.input1 = 10
        self.input2 = 20

    # pytest looks for all methods that start with "test_"
    def test_one(self):
        # there are many different asserts, see full list above
        #  here we are testing if our function "adder" adds 10+20 correctly and equals 30
        self.assertEqual(mycode.adder(10,20),30)

    # we can write as many tests as we like,
    #   here is the same test input/out but with our setUp variables
    def test_two(self):
        self.assertEqual(mycode.adder(self.input1,self.input2),30)
  • To run pytest, we type the following in the terminal

python -m pip install pytest pytest-cov
python -m pytest --cov-report=html --cov='.'
  • The output will look something like:

============================= test session starts =============================
platform win32 -- Python 3.8.0, pytest-5.2.2, py-1.8.0, pluggy-0.13.0
rootdir: C:\Users\yourusername\Desktop
collected 2 items

test_mycode.py ..                                                        [100%]

============================== 2 passed in 0.03s ==============================

2.18.2. How to check if a function raises an error

Reusing the same example from mycode.py

# filename: test_mycode.py

import unittest
import mycode


class test_mycode(unittest.TestCase):

    def test_error(self):
        # to test a error raise, we have to enclose the code being testing a "with" block
        #  here we are testing if our code raises a TypeError when adding 10 + "20" as it should
        with self.assertRaises(TypeError):
            mycode.adder(10,"20")

2.18.3. How to report out code test coverage

Code test coverage writes out a detailed report on what percent of your code the test actually executed.

python -m pytest --cov-report=html --cov='.'

You can also write out a single xml coverage file. This is useful for CI (continuous integration) since you only have to point your upload/file to 1 file.

python -m pytest --cov-report=xml --cov='.'

2.18.4. To mock file-read without an actual file

f = io.StringIO("text\n")

f.readline()
>>> "text"