CIT228 - Advanced Database Systems

 Testing your Code


I. Overview

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


II. Unit Testing in Python

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.

A. Unit Testing on Functions

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.  Creating a unit test:

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'."""

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')

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

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

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

import unittest
from name_function import get_formatted_name

class NamesTestCase(unittest.TestCase):
    """Tests 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()  

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

    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')

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

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)

2.  Modifying a Function and Unit Test

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:

class NamesTestCase(unittest.TestCase):
    """Tests 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')

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

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

Hands On #1 -Practicing unit testing on functions

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)

from city_functions import get_formatted_city

print("Enter 'q' at any time to quit.")
while True:
    city = input("\nPlease enter a city: ")
    if city == 'q':
        break
    country = input("Please enter a country: ")
    if country == 'q':
        break
    population = input("Please enter the population: ")
    if population == 'q':
        break
    formatted_city = get_formatted_city(city,country,population)
    print(f"\tFormatted City Information: {formatted_city}.")

 

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


B.  Unit Testing on Classes

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)

class AnonymousSurvey:
    """Collect anonymous answers to a survey question."""
    
    def __init__(self, question):
        """Store a question, and prepare to store responses."""
        self.question = question
        self.responses = []
        
    def show_question(self):
        """Show the survey question."""
        print(self.question)
        
    def store_response(self, new_response):
        """Store a single response to the survey."""
        self.responses.append(new_response)
        
    def show_results(self):
        """Show all the responses that have been given."""
        print("Survey results:")
        for response in self.responses:
            print(f"- {response}")

Sample program that uses the AnonymousSurvey class:  language_survey.py

from survey import AnonymousSurvey

# Define a question, and make a survey.
question = "What language did you first learn to speak?"
my_survey = AnonymousSurvey(question)

# Show the question, and store responses to the question.
my_survey.show_question()
print("Enter 'q' at any time to quit.\n")
while True:
    response = input("Language: ")
    if response == 'q':
        break
    my_survey.store_response(response)

# Show the survey results.
print("\nThank you to everyone who participated in the survey!")
my_survey.show_results()

1.  Setting up a simple test class

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.

    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']

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

    def store_response(self, new_response):
        """Store a single response to the survey."""
        self.responses.append(new_response)

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). 

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

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). 

    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)

2.  Setting up a test class that passes multiple parameters in setUp()

We are going to set up a unit test for the class shown below:

class Store:
    def __init__(self,store_name,store_type, daily_sales=0.0):
        self.store_name=store_name
        self.store_type=store_type
        self.daily_sales=daily_sales

    def describe_store(self):
        print(f"{self.store_name} is a  {self.store_type} shop.")    

    def open_store(self):
        print(f"{self.store_name} is open.")   

    def set_daily_sales(self,sales):
        self.daily_sales = float(sales)    

    def increment_daily_sales(self,sales):
        self.daily_sales += float(sales) 

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

import unittest
from store import Store

class TestStore(unittest.TestCase):
    """Unit Tests for the Store 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.

    def setUp(self):
        store_name="Betty's Bargain Basement"
        store_type="Outlet"
        daily_sales=15000
        self.store = Store(store_name, store_type,daily_sales)

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

    def test_set_daily_sales_int(self):
        sales=100000
        self.store.set_daily_sales(sales)
        self.assertEqual(self.store.daily_sales,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.

    def test_set_daily_sales_string(self):
        sales="100000"
        self.store.set_daily_sales(sales)
        self.assertEqual(self.store.daily_sales,100000)   

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

    def test_increment_daily_sales_float(self):
        sales=200.50
        self.store.increment_daily_sales(sales)
        self.assertEqual(self.store.daily_sales,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.

    def test_increment_daily_sales_string(self):
        sales="100"
        self.store.increment_daily_sales(sales)
        self.assertEqual(self.store.daily_sales,15100)

Step 7:  Add the if statement that ensures the current program is the one that should run and then executes the unit test.

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

Full unit test program - test_store.py

import unittest
from store import Store

class TestStore(unittest.TestCase):
    """Unit Tests for the Store class """
    
    def setUp(self):
        store_name="Betty's Bargain Basement"
        store_type="Outlet"
        daily_sales=15000
        self.store = Store(store_name, store_type,daily_sales)

    def test_set_daily_sales_int(self):
        sales=100000
        self.store.set_daily_sales(sales)
        self.assertEqual(self.store.daily_sales,100000)

    def test_set_daily_sales_string(self):
        sales="100000"
        self.store.set_daily_sales(sales)
        self.assertEqual(self.store.daily_sales,100000)    

    def test_increment_daily_sales_float(self):
        store=200.50
        self.store.increment_daily_sales(store)
        self.assertEqual(self.store.daily_sales,15200.50)  

    def test_increment_daily_sales_string(self):
        store="100"
        self.store.increment_daily_sales(store)
        self.assertEqual(self.store.daily_sales,15100)

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

Sample Output:

....
----------------------------------------------------------------------
Ran 4 tests in 0.001s

OK


Hands On #2 - Creating unit testing for classes

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


Hands On #3 - Uploading to GitHub

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!)