Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
pierian-data
GitHub Repository: pierian-data/complete-python-3-bootcamp
Path: blob/master/07-Errors and Exception Handling/04-Unit Testing.ipynb
671 views
Kernel: Python [conda env:base] *


Content Copyright by Pierian Data

Unit Testing

Equally important as writing good code is writing good tests. Better to find bugs yourself than have them reported to you by end users!

For this section we'll be working with files outside the notebook. We'll save our code to a .py file, and then save our test script to another .py file. Normally we would code these files using a text editor like Brackets or Atom, or inside an IDE like Spyder or Pycharm. But, since we're here, let's use Jupyter!

Recall that with some IPython magic we can write the contents of a cell to a file using %%writefile.
Something we haven't seen yet; you can run terminal commands from a jupyter cell using !

Testing tools

There are dozens of good testing libraries out there. Most are third-party packages that require an install, such as:

These are simple tools that merely look at your code, and they'll tell you if there are style issues or simple problems like variable names being called before assignment.

A far better way to test your code is to write tests that send sample data to your program, and compare what's returned to a desired outcome.
Two such tools are available from the standard library:

Let's look at pylint first, then we'll do some heavier lifting with unittest.

pylint

pylint tests for style as well as some very basic program logic.

First, if you don't have it already (and you probably do, as it's part of the Anaconda distribution), you should install pylint.
Once that's done feel free to comment out the cell, you won't need it anymore.

! pip install pylint

Let's save a very simple script:

%%writefile simple1.py a = 1 b = 2 print(a) print(B)
Overwriting simple1.py

Now let's check it using pylint

! pylint -r y simple1.py
************* Module simple1 simple1.py:1:0: C0114: Missing module docstring (missing-module-docstring) simple1.py:1:0: C0103: Constant name "a" doesn't conform to UPPER_CASE naming style (invalid-name) simple1.py:2:0: C0103: Constant name "b" doesn't conform to UPPER_CASE naming style (invalid-name) simple1.py:4:6: E0602: Undefined variable 'B' (undefined-variable) Report ====== 4 statements analysed. Statistics by type ------------------ +---------+-------+-----------+-----------+------------+---------+ |type |number |old number |difference |%documented |%badname | +=========+=======+===========+===========+============+=========+ |module |1 |1 |= |0.00 |0.00 | +---------+-------+-----------+-----------+------------+---------+ |class |0 |NC |NC |0 |0 | +---------+-------+-----------+-----------+------------+---------+ |method |0 |NC |NC |0 |0 | +---------+-------+-----------+-----------+------------+---------+ |function |0 |1 |-1.00 |0 |0 | +---------+-------+-----------+-----------+------------+---------+ 6 lines have been analyzed Raw metrics ----------- +----------+-------+------+---------+-----------+ |type |number |% |previous |difference | +==========+=======+======+=========+===========+ |code |5 |83.33 |NC |NC | +----------+-------+------+---------+-----------+ |docstring |0 |0.00 |NC |NC | +----------+-------+------+---------+-----------+ |comment |0 |0.00 |NC |NC | +----------+-------+------+---------+-----------+ |empty |1 |16.67 |NC |NC | +----------+-------+------+---------+-----------+ Duplication ----------- +-------------------------+------+---------+-----------+ | |now |previous |difference | +=========================+======+=========+===========+ |nb duplicated lines |0 |0 |0 | +-------------------------+------+---------+-----------+ |percent duplicated lines |0.000 |0.000 |= | +-------------------------+------+---------+-----------+ Messages by category -------------------- +-----------+-------+---------+-----------+ |type |number |previous |difference | +===========+=======+=========+===========+ |convention |3 |0 |0 | +-----------+-------+---------+-----------+ |refactor |0 |0 |0 | +-----------+-------+---------+-----------+ |warning |0 |0 |0 | +-----------+-------+---------+-----------+ |error |1 |0 |0 | +-----------+-------+---------+-----------+ Messages -------- +-------------------------+------------+ |message id |occurrences | +=========================+============+ |invalid-name |2 | +-------------------------+------------+ |undefined-variable |1 | +-------------------------+------------+ |missing-module-docstring |1 | +-------------------------+------------+ -------------------------------------------------------------------- Your code has been rated at 0.00/10 (previous run: 10.00/10, -10.00)

Pylint first lists some styling issues - modules and function definitions should have descriptive docstrings, and single characters are a poor choice for variable names.

More importantly, however, pylint identified an error in the program - a variable called before assignment. This needs fixing.

Note that pylint scored our program 0 out of 10. Let's try to improve that!

%%writefile simple1.py """ A very simple script. """ def myfunc(): """ An extremely simple function. """ first = 1 second = 2 print(first) print(second) myfunc()
Overwriting simple1.py
! pylint -r y simple1.py
Report ====== 6 statements analysed. Statistics by type ------------------ +---------+-------+-----------+-----------+------------+---------+ |type |number |old number |difference |%documented |%badname | +=========+=======+===========+===========+============+=========+ |module |1 |1 |= |100.00 |0.00 | +---------+-------+-----------+-----------+------------+---------+ |class |0 |NC |NC |0 |0 | +---------+-------+-----------+-----------+------------+---------+ |method |0 |NC |NC |0 |0 | +---------+-------+-----------+-----------+------------+---------+ |function |1 |NC |NC |100.00 |0.00 | +---------+-------+-----------+-----------+------------+---------+ 16 lines have been analyzed Raw metrics ----------- +----------+-------+------+---------+-----------+ |type |number |% |previous |difference | +==========+=======+======+=========+===========+ |code |7 |43.75 |5 |+2.00 | +----------+-------+------+---------+-----------+ |docstring |6 |37.50 |NC |NC | +----------+-------+------+---------+-----------+ |comment |0 |0.00 |NC |NC | +----------+-------+------+---------+-----------+ |empty |3 |18.75 |1 |+2.00 | +----------+-------+------+---------+-----------+ Duplication ----------- +-------------------------+------+---------+-----------+ | |now |previous |difference | +=========================+======+=========+===========+ |nb duplicated lines |0 |0 |0 | +-------------------------+------+---------+-----------+ |percent duplicated lines |0.000 |0.000 |= | +-------------------------+------+---------+-----------+ Messages by category -------------------- +-----------+-------+---------+-----------+ |type |number |previous |difference | +===========+=======+=========+===========+ |convention |0 |3 |3 | +-----------+-------+---------+-----------+ |refactor |0 |0 |0 | +-----------+-------+---------+-----------+ |warning |0 |0 |0 | +-----------+-------+---------+-----------+ |error |0 |1 |1 | +-----------+-------+---------+-----------+ Messages -------- +-----------+------------+ |message id |occurrences | +===========+============+ -------------------------------------------------------------------- Your code has been rated at 10.00/10 (previous run: 0.00/10, +10.00)

Excellent! Our score climbed to 10 out of 10. But what if the problem was more complex?

%%writefile simple2.py """ A very simple script. """ def myfunc(): """ An extremely simple function. """ first = 1 second = 2 print(first) print('second') myfunc()
Overwriting simple2.py
! pylint -r y simple2.py
************* Module simple2 simple2.py:10:4: W0612: Unused variable 'second' (unused-variable) Report ====== 6 statements analysed. Statistics by type ------------------ +---------+-------+-----------+-----------+------------+---------+ |type |number |old number |difference |%documented |%badname | +=========+=======+===========+===========+============+=========+ |module |1 |1 |= |100.00 |0.00 | +---------+-------+-----------+-----------+------------+---------+ |class |0 |NC |NC |0 |0 | +---------+-------+-----------+-----------+------------+---------+ |method |0 |NC |NC |0 |0 | +---------+-------+-----------+-----------+------------+---------+ |function |1 |1 |= |100.00 |0.00 | +---------+-------+-----------+-----------+------------+---------+ 16 lines have been analyzed Raw metrics ----------- +----------+-------+------+---------+-----------+ |type |number |% |previous |difference | +==========+=======+======+=========+===========+ |code |7 |43.75 |NC |NC | +----------+-------+------+---------+-----------+ |docstring |6 |37.50 |NC |NC | +----------+-------+------+---------+-----------+ |comment |0 |0.00 |NC |NC | +----------+-------+------+---------+-----------+ |empty |3 |18.75 |NC |NC | +----------+-------+------+---------+-----------+ Duplication ----------- +-------------------------+------+---------+-----------+ | |now |previous |difference | +=========================+======+=========+===========+ |nb duplicated lines |0 |0 |0 | +-------------------------+------+---------+-----------+ |percent duplicated lines |0.000 |0.000 |= | +-------------------------+------+---------+-----------+ Messages by category -------------------- +-----------+-------+---------+-----------+ |type |number |previous |difference | +===========+=======+=========+===========+ |convention |0 |0 |0 | +-----------+-------+---------+-----------+ |refactor |0 |0 |0 | +-----------+-------+---------+-----------+ |warning |1 |1 |1 | +-----------+-------+---------+-----------+ |error |0 |0 |0 | +-----------+-------+---------+-----------+ Messages -------- +----------------+------------+ |message id |occurrences | +================+============+ |unused-variable |1 | +----------------+------------+ ------------------------------------------------------------------ Your code has been rated at 8.33/10 (previous run: 8.33/10, +0.00)

pylint tells us there's an unused variable in line 10, but it doesn't know that we might get an unexpected output from line 12! For this we need a more robust set of tools. That's where unittest comes in.

unittest

unittest lets you write your own test programs. The goal is to send a specific set of data to your program, and analyze the returned results against an expected result.

Let's generate a simple script that capitalizes words in a given string. We'll call it cap.py.

%%writefile cap.py def cap_text(text): return text.capitalize()
Overwriting cap.py

Now we'll write a test script. We can call it whatever we want, but test_cap.py seems an obvious choice.

When writing test functions, it's best to go from simple to complex, as each function will be run in order. Here we'll test simple, one-word strings, followed by a test of multiple word strings.

%%writefile test_cap.py import unittest import cap class TestCap(unittest.TestCase): def test_one_word(self): text = 'python' result = cap.cap_text(text) self.assertEqual(result, 'Python') def test_multiple_words(self): text = 'monty python' result = cap.cap_text(text) self.assertEqual(result, 'Monty Python') if __name__ == '__main__': unittest.main()
Overwriting test_cap.py
! python test_cap.py
F. ====================================================================== FAIL: test_multiple_words (__main__.TestCap.test_multiple_words) ---------------------------------------------------------------------- Traceback (most recent call last): File "/Users/marci/GIT/Udemy/Complete-Python-3-Bootcamp/07-Errors and Exception Handling/test_cap.py", line 14, in test_multiple_words self.assertEqual(result, 'Monty Python') AssertionError: 'Monty python' != 'Monty Python' - Monty python ? ^ + Monty Python ? ^ ---------------------------------------------------------------------- Ran 2 tests in 0.000s FAILED (failures=1)

What happened? It turns out that the .capitalize() method only capitalizes the first letter of the first word in a string. Doing a little research on string methods, we find that .title() might give us what we want.

%%writefile cap.py def cap_text(text): return text.title() # replace .capitalize() with .title()
Overwriting cap.py
! python test_cap.py
.. ---------------------------------------------------------------------- Ran 2 tests in 0.000s OK

Hey, it passed! But have we tested all cases? Let's add another test to test_cap.py to see if it handles words with apostrophes, like don't.

In a text editor this would be easy, but in Jupyter we have to start from scratch.

%%writefile test_cap.py import unittest import cap class TestCap(unittest.TestCase): def test_one_word(self): text = 'python' result = cap.cap_text(text) self.assertEqual(result, 'Python') def test_multiple_words(self): text = 'monty python' result = cap.cap_text(text) self.assertEqual(result, 'Monty Python') def test_with_apostrophes(self): text = "monty python's flying circus" result = cap.cap_text(text) self.assertEqual(result, "Monty Python's Flying Circus") if __name__ == '__main__': unittest.main()
Overwriting test_cap.py
! python test_cap.py
..F ====================================================================== FAIL: test_with_apostrophes (__main__.TestCap.test_with_apostrophes) ---------------------------------------------------------------------- Traceback (most recent call last): File "/Users/marci/GIT/Udemy/Complete-Python-3-Bootcamp/07-Errors and Exception Handling/test_cap.py", line 19, in test_with_apostrophes self.assertEqual(result, "Monty Python's Flying Circus") AssertionError: "Monty Python'S Flying Circus" != "Monty Python's Flying Circus" - Monty Python'S Flying Circus ? ^ + Monty Python's Flying Circus ? ^ ---------------------------------------------------------------------- Ran 3 tests in 0.000s FAILED (failures=1)

Now we have to find a solution that handles apostrophes! There is one (look up capwords from the string module) but we'll leave that as an exercise for the reader.

Great! Now you should have a basic understanding of unit testing!