Chapter 11 covers how to test code using Pythons unittest module. You'll learn how to test classes and functions and what a passing test vs a failing test look like. You will also learn how to validate input and output.
To see an explanation of all lecture material in chapter 11, watch: https://youtu.be/EICNUVMQG_I
To see sample exercises that go with the Try It Yourself sections in the chapter, download: chapter_11.zip
Testing code can be done manually (which is what we have been doing by running programs to make sure they work and verifying input and output).
Testing code can also be automated using a wide variety of techniques. We are going to be looking at unit testing which involves automating testing for the smallest piece of code such as a function or class.
Testing has it's own terminology. Common terms include:
Unit Test - verifies one aspect of a modules behavior is correct
Test Case - Includes ALL input a module is expected to receive and output it is expected to produce
1. Create a new file and import the unittest class and the function you want to test
Syntax:
import unittest
from filename import functionName
Example from book:
import unittest
from name_function import get_formatted_name
2. Create a new class that inherits from unittest.TestCase
Syntax:
class TestNewClassName(unittest.TestCase):
NOTE: It is standard practice to include the word "test" in the class name
Example:
class NamesTestCase(unittest.TestCase):
3. Create a method to test the function
The test class can test one function or it can test many functions.
In the test method, you need to call the function and pass it the data it needs for processing. After you call the function, you will check the return value to see if it is what you expected.
In the unittest.TestCase class, there are several methods you can use to check the values returned. These methods are called assert methods. The table below outlines the more widely used assert methods:
Method |
Use |
assertEqual(a, b) |
Verify that 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) |
Verify that item is not in list |
assertIsInstance(a,b) |
Verify that a is an instance of b |
assertNotIsInstance(a,b) |
Verify a is not an instance of b |
assertIs(a,b) |
Verify a is b |
assertIsNot(a,b) |
Verify a is not b |
assertIsNone(x) |
Verify that x is None |
assertisNotNone(x) |
Verify that x is not None |
For more information on assert methods and unit testing, see: https://docs.python.org/3/library/unittest.html
Since we want to check to see if the return value is equal to specific, formatted output, we are going to use self.assertEqual. If the output is what we expect, self.assertEqual will return TRUE; otherwise, it will return FALSE and the test will fail.
IMPORTANT: When you create your test method, make sure you start it with test_ Any method beginning with test_ will automatically run when the program is run.
Syntax:
returnValue = functionCall(argument1, argument2, argumentN)
self.assertEqual(returnValue, expectedReturnValue)
Example from the book showing 1 function test:
"""Tests for 'name_function.py'."""
4. Code the statement that calls unittest.main() at the bottom of the class
The unit test is run at the main program. If the program you are executing is the main program, Python's test class sets the variable __name__ equal to main which lets Python's unit test know to run the current program. The unittest.main() statement actually runs the test case
5. Test Outcomes
To run the unit test, you simply run the testing program you created.
There are three types of possible test outcomes :
Entire programming example of test_name_function.py
Output of a successful test:
.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
If we add another test function and change the expected output to all lowercase, the test_first_last_nameV2 method in the unit test should fail
Sample Output:
.F
======================================================================
FAIL: test_first_last_nameV2 (__main__.NamesTestCase)
Do names like 'Janis Joplin' work?
----------------------------------------------------------------------
Traceback (most recent call last):
File "c:\Users\Lisa.LAPTOP-B4JF6RME\Documents\CIT228-AnswerKey\Chapter11\test_name_function.py", line 15, in test_first_last_nameV2
self.assertEqual(formatted_name, 'janis joplin')
AssertionError: 'Janis Joplin' != 'janis joplin'
- Janis Joplin
? ^ ^
+ janis joplin
? ^ ^
----------------------------------------------------------------------
Ran 2 tests in 0.001s
FAILED (failures=1)
If you modify the INPUT or OUTPUT within a function, then you will need to modify the unit test or you will get an error when you try to run it.
If you modify a function, but you DO NOT change the INPUT or OUTPUT, then you don't need to modify the unit test.
The unit test is only concerned with the input and output.
Example of a modification in name_function.py
def get_formatted_name(first, last, middle):
"""Generate a neatly formatted full name."""
if middle:
full_name = f"{first} {middle} {last}"
else:
full_name = f"{first} {last}"
return full_name.title()
Running the unit test without modifcation will result in an error because it is receiving 3 parameters instead of 2
If you don't want to modify the unit test, you can make the middle name optional by adding a default value.
Revised modification to name_function.py
def get_formatted_name(first, last, middle=""):
"""Generate a neatly formatted full name."""
if middle:
full_name = f"{first} {middle} {last}"
else:
full_name = f"{first} {last}"
return full_name.title()
With the default value, the unit test can run without having to modify the code.
Example of the unit test testing for first and last name plus first, last and middle names:
To see a video explanation, watch: https://youtu.be/iQZOC-1mgd4
1. Open the CIT228 folder and create a new folder named Chapter11
2. Complete Try It Yourself 11-1 and 11-2 on pages 215-216 (you should have 2 files named city_functions.py and test_cities.py when you are done)
NOTE: in city_functions.py, you can make population a string parameter since we are not going to perform arithmetic on it
Sample Output from test_cities.py:
..
----------------------------------------------------------------------
Ran 2 tests in 0.000s
OK
NOTE: Until you get the hang of unit testing, you may find it helpful to create a program called use_cities.py that you can run the function through before you perform unit testing on it. If you don't manually test the function first, it is hard to know if the function is miscoded or if the unit test isn't coded properly. Once you are familiar with unit testing, you can omit this step.
Example of use_cities.py (optional)
Sample Output from use_cities.py
Enter 'q' at any time to quit.
Please enter a city: London
Please enter a country: England
Please enter the population: 15,000,000
Formatted City Information: London, England - population 15,000,000.
Please enter a city: Madrid
Please enter a country: Spain
Please enter the population:
Formatted City Information: Madrid, Spain.
Please enter a city: q
Testing classes is similar to functions because you are testing the behavior of the class methods. It is a little more complex because you need to create a test class object that contains an instance of the class you are testing. You must use this object to call the methods you want to test.
Example: AnonymousSurvey class from book: survey.py (this is the class you want to test)
Sample program that uses the AnonymousSurvey class: language_survey.py
In a unit test for a class, the first method is the setUp() method. This method is used to create an instance of the class you are testing. This instance is recreated before EVERY test that you perform so each test method has a fresh instance of the class for testing purposes. Changes you make to class properties in one method will not have any impact testing the next method because all values are reset.
Example:
In our test class setUp() method, we create an instance of the AnonymousSurvey class called self.my_survey and we create a list of responses called self.responses. Prefixing the names with self_ gives any method in the test class access to them. For each method we test, self.responses is reset to English, Spanish and Mandarin and the question is reset to "What language did you first learn to speak?"
IMPORTANT: You need to create variables and test data for ALL parameters required in the class you are testing. In the example below, the only required parameter is question.
Other methods you include in the test class are ones whose input or output you need to verify. We are testing the store_response method to ensure it is storing the lists correctly. We are making sure the first item in the list is stored correctly and then we are making sure all items in the list are stored corrected.
Original store_responses() method in the AnonymousSurvey class
Here are the 2 tests we are gong to conduct
Test#1: the self.my_survey object is used to call the store_response method. It is passing the first response stored in self.responses which is English. The store_response method in the AnonymousSurvey class then adds the response to it's responses list. Our test method compares what is in self.responses[0] which is London, to what the AnonymousSurvey class stored in it's response list (self.my_survey.responses is accessing the response list in the AnonymousSurvey class).
Test #2: setUp() is run and the values are reset. So when we start this method, nothing is in AnonymousSurvey's response list. The first for in loop uses the self.my_survey object to call the store_response method and it passes each language stored in our test response list which is named self.responses. So, it passes English, then Spanish and finally Mandarin to AnonymousSurvey's store_response method. To ensure that the entire list is stored accurately, we have a second for in loop that checks AnonymousSurvey's response list for each language in our self.response list to make sure the list was created accurately (self.my_survey.responses represents the entire list of responses that was created in the AnonymousSurvey's store_response method).
We are going to set up a unit test for the class shown below:
We are going to test set_daily_sales and increment_daily_sales. We'll call the file test_store.py
Step 1: import files at the top of test_store.py and create the test class
Step 2: Create the setUp() method. Create variables for all 3 parameters. Create a test object called self.store by calling the the Store class and passing the arguments.
Step 3: Create the first test method for set_daily_sales and pass it a number. self.store is the object that is calling set_daily_sales in the Store class. We are then checking the value that was set by the Store class (self.store.daily_sales) against the value that should be stored 100000
Step 4: Create a second test method for set_daily_sales and pass it a string (the method in the Store class should convert it to float). This method works the same as the previous one, but it is checking to make sure that string data is converted into a number correctly. You should note that because the daily sales doesn't have any trailing decimal places, they have been omitted even though Store is converting the string into float.
Step 5: Create the first test method for increment_daily_sales and pass it a float value. self.store is the object that is calling increment_daily_sales which is part of the Store class. We are checking the value added to daily_sales to make sure it is accurate. REMEMBER that before this test is run, setUp() resets daily_sales to 15000, so we would expect daily_sales to be 15200.50 after the method is done running. We use assertEqual to make sure that the daily_sales stored in the self.store object is equal to 15200.50
Step 6: Create a second test method of increment_daily_sales that passes a string value. This method works the same as the previous one, it is just making sure the string is converted to a float value properly.
Step 7: Add the if statement that ensures the current program is the one that should run and then executes the unit test.
Full unit test program - test_store.py
Sample Output:
....
----------------------------------------------------------------------
Ran 4 tests in 0.001s
OK
To see a video explanation, watch https://youtu.be/LOIahXrOHcY
1. Copy your Chapter9/restaurant.py file into Chapter11
2. Create a unit test called test_restaurant.py. Create a setUp() method and methods to test set_number_served() and increment_number_served() HINT: This is very similar to the store example above.
3. Complete Try It Yourself 11-3 on page 221 Name the program with the Employee class employee.py. Name the program that includes unit tests, test_employee.py
1. You should have the following files in your Chapter11 folder:
| Exercise | Assignment Files | Points |
|---|---|---|
| Hands On 1: Try It Yourself 11-1 and 11-2 | city_functions.py and test_cities.py | 6 points |
| Hands On 2: Try It Yourself 11-3 | restaurant.py and test_restaurant.py employee.py and test_employee.py |
14 points |
2. Upload Chapter11 to GitHub
a. Click the ... next to CIT228 Git and select the Changes menu, Stage all Changes command.
b. In the Message window below Source Control, enter Chapter11 and then click the ... next to CIT228 Git and select the Commit menu, Commit Staged command
c. Display the menu next to the CIT228 Git folder by selecting ... and select the Push command
d. This will put all files and folders inside your CIT228/Chapter11 directory into your GitHub repository
3. Open a web browser and go to your CIT228 repository. Make sure the Chapter11 directory and files are there, then copy the URL and paste it into the lab assignment dropbox (you are done with the second part of your lab assignment!)