python-crash-course-book/chap11_testing.md

6.6 KiB

Chapter 11. Testing Your Code

Learn how to test code using Python's unittest module.

Testing a Function

Function to be tested as shown below.

def get_formatted_name(first, last):
    """Generate a neatly formatted full name."""
    full_name = f"{first} {last}"
    return full_name.title()

A script that should use get_formatted_name

from name_function import get_formatted_name

print("Enter 'q' at any time to quit.")
while True:
    first = input("\nPlease give me a first name: ")
    if first == 'q':
        break
    last = input("Please give me a last name: ")
    if last == 'q':
        break

    formatted_name = get_formatted_name(first, last)
    print(f"\tNeatly formatted name: {formatted_name}.")

To make sure get_formatted_name function properly, using unittest module of Python

import unittest
from name_function import get_formatted_name

class NamesTestCase(unittest.TestCase):
    """Test for name_function.py"""

    def test_first_last_name(self):
        """Do names like 'Janis Joplin' work?"""
        formatted_name = get_formatted_name('janis', 'joplin')
        self.assertEqual(formatted_name, 'Janis Joplin')

if __name__ == '__main__':
    unittest.main()

Key points:

  • Import unittest module as shown
  • Import function to be tested
  • Here, unittest's most useful feature assert is used. assertEqual can compare two objects
  • __name__ is a special variable, which is set when the program is executed. If the file is being run as the main program, the value of __name__ is set to '__main__'

Result of running this test:

.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK
  • First dot represent the single test passed.

A Failing Test

Failure case returns

F
======================================================================
FAIL: test_first_last_name (__main__.NamesTestCase)
Do names like 'Janis Joplin' work?
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test_name_function.py", line 10, in test_first_last_name
    self.assertEqual(formatted_name, 'Janis Joplins')
AssertionError: 'Janis Joplin' != 'Janis Joplins'
- Janis Joplin
+ Janis Joplins
?             +


----------------------------------------------------------------------
Ran 1 test in 0.001s

FAILED (failures=1)

Highlight:

  • FAIL: test_first_last_name tell's where the error coming from.

Fix them

If employing TDD, we can first create test case, then develop code to be test against.

Testing a Class

e.g. above shown how to write test case for single function, we also need to write test for class.

A Variety of Assert Methods

Python unittest module provide various assert methods in unittest.TestCase class.

List of available assert methods

Method Use
assertEqual(a,b) Verify a == b
assertNotEqual(a,b) Verify that a != b
assertTrue(x) Verify that x is True
assertFalse(x) Verify that x is False
assertIn(item, list) Verify that item is in list
assertNotIn(item, list) Verrify that item is not in list

A Class to Test

Steps to write test case:

  1. Importing unittest module and class to test
  2. Creating a test class to inherit from unittest.TestCase
  3. Creating test function under class
    1. Create an instance of the class to be tested.
    2. Verify its operation

e.g.

  • class to be tested in survey.py have class AnonymousSurvey
  • script to use class language_survey
  • test script as shown below:
import unittest
from survey import AnonymousSurvey

class TestAnonymousSurvey(unittest.TestCase):
    """Tests for the class AnonymouseSurvey"""
    
    def test_store_single_response(self):
        """Tests that a single response is stored properly"""
        question = "What language did you first learn to speak?"
        my_survey = AnonymousSurvey(question)
        my_survey.store_response('English')
        self.assertIn('English', my_survey.responses)

    def test_store_three_responses(self):
        """Test that three individual responses are stored properly"""
        question = "What language did you first learn to speak?"
        my_survey = AnonymousSurvey(question)
        responses = ['English', 'Spanish', 'Mandarin']
        
        # Store responses
        for response in responses:
            my_survey.store_response(response)
        
        # Test my_survey operation
        for response in responses:
            self.assertIn(response, my_survey.responses)

if __name__ == '__main__':
    unittest.main()

We can add more test functions to test different scenario. But it's too repetitive. Use setUp()

The setUp() Method

We can use setUp() to setup initial states of self.my_survey, which can reduce repetitions.

import unittest
from survey import AnonymousSurvey

class TestAnonymousSurvey(unittest.TestCase):
    """Tests for the class AnonymouseSurvey"""
    
    def setUp(self):
        """
        Create a survey and a set of responses for use in all test methods.
        """
        question = "What language did you first learn to speak?"
        self.my_survey = AnonymousSurvey(question)
        self.responses = ['English', 'Spanish', 'Mandarin']

    def test_store_single_response(self):
        """Test test a single response is stored properly"""
        self.my_survey.store_response(self.responses[0])
        self.assertIn(self.responses[0], self.my_survey.responses)

    def test_store_three_responses(self):
        """Test that three individual responses are stored properly."""
        for response in self.responses:
            self.my_survey.store_response(response)
        for response in self.responses:
            self.assertIn(response, self.my_survey.responses)

if __name__ == '__main__':
    unittest.main()
  • Use setUp() can make test methods easier to write.
  • We can make one set of instances and attributes in setUp() and then use these instances in all your test methods.

Note of unittest

  • When a test case is running, Python prints one character for each unit test as it's completed.
  • A passing test prints a dot .
  • A test that results in an error prints an E
  • A test that results in failed assertion prints an F.
  • Hence, we can see a different number of dots and characters on the first line of output when you run your test cases.