Advanced Python - 4. testing Your Code
4.1.1 Creating Something to Test
4.1.2 Unit Tests and Test Cases
4. Testing Your Code
- testing proves that your code works as it’s supposed to in response to all the input types it’s designed to receive. We will use Python’s unittest module.
- Unit tests are pieces of code written to test other pieces of code, typically a single function or method, that we refer to as a unit.
- These tests are a very important part of the software development process, as they help to ensure that code works as intended and catch bugs early on.
4.1 testing a Function
4.1.1 Creating Something to Test
- We need to create a simple function to run tests on. Here, we are going to take in a first and last name, and return a neatly formatted full name. This program will be saved as name_function.py:
def get_formatted_name(first, last):
full_name = first + ‘ ‘ + last
return full_name.title() # Capitalizes the first letter
- To check that get_formatted_name() works, let’s make a program that uses this function. The program names.py lets users enter a first and last name, and see a neatly formatted full 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(“\tNeatly formatted name: “ + formatted_name + ‘.’)
OUTPUT:
Enter ‘q’ at any time to quit.
Please give me a first name: janis
Please give me a last name: joplin
Neatly formatted name: Janis Joplin
Please give me a first name: bob
Please give me a last name: dylan
Neatly formatted name: Bob Dylan
Please give me a first name: q
- So far, everything is working well. But let’s say we want to modify get_formatted_name() so that it can handle middle names. As we change the code, we want to make sure we don’t break the way the function handles names that only have a first and last name.
- We could test our code by running names.py and entering names like Janis Joplin every time we make changes, but that would waste a lot of time.
- We need to automate the testing.
4.1.2 Unit Tests and Test Cases
- The module unittest from the Python standard library provides tools for testing code.
- A unit test confirms that one specific aspect of a function’s behavior is correct.
- A test case is a collection of unit tests that together prove that a function behaves as it’s supposed to, within the full range of situations you expect it to handle. A good test case considers all the possible kinds of input a function could receive and includes tests to represent each of these situations.
- A test case with full coverage includes a full range of unit tests covering all the possible ways you can use a function.
- To write a test case for a function, import the unittest module and the function you want to test. Then create a class that inherits from unittest.TestCase, and write a series of methods to test different aspects of your function’s behavior.
- Here is a test case with one method that verifies that the function get_formatted_name() works correctly when given a first and last name:
import unittest
from name_function import get_formatted_name
class NamesTestCase(unittest.TestCase):
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’)
unittest.main()
- We created a class called NamesTestCase, which will contain a series of unit tests for get_formatted_name(). It’s good to have the word Test in the class name. This class must inherit from the class unittest.TestCase so Python knows how to run the tests you write.
- Any method that starts with test_ will be run automatically when we run test_name_function.py. Within this test method, we call the function we want to test and store a return value that we are interested in testing. In this example we call get_formatted_name() with the arguments ‘janis’ and ‘joplin’, and store the result in formatted_name.
- We then use a very useful feature of unittest, which is an assert method. Assert methods verify that a result you received matches the result you expect to receive. The line:
self.assertEqual(formatted_name, ‘Janis Joplin’)
says, “Compare the value in formatted_name to the string ‘Janis Joplin’. If they are equal
as expected, fine. But if they don’t match, let me know!”
- The line unittest.main() tells Python to run the tests in this file. When we run test_name_function.py, we get the following output:
.
—-------------------------------------------------------
Ran 1 test in 0.000s
OK
- The dot on the first line of output tells us that a single test passed.
- The next line tells us that Python ran one test, and it took less than 0.001 seconds to run.
- The final OK tells us that all unit tests in the test case passed.
- This output indicates that the function will always work for names that have a first and last name unless we modify (change) the function.
- When we modify the function get_formatted_name(), we can run this test again. If the test case passes, we know the function will still work for names like Janis Joplin.
4.1.3 A Failing Test
- Let’s change the get_formatted_name() function so that it handles middle names, but we will do it in a way that breaks the function for names with just a first and last name. We will save it as name_function.py:
def get_formatted_name(first, middle, last):
full_name = first + ‘ ‘ + middle + ‘ ‘ + last
return full_name.title()
- This version will work for people with middle names, but when we test it, we see that we’ve broken the function for people with just a first and last name. This time, running the file test_name_function.py gives this output (the red numbers are for explanation below):
1- E
=========================================
2- ERROR: test_first_last_name (main.NamesTestCase)
—---------------------------------------------------------------------
3- Traceback (most recent call last):
File “test_name_function.py”, line 8, in test_first_last_name
formatted_name = get_formatted_name(‘janis’, ‘joplin’)
TypeError: get_formatted_name() missing 1 required positional argument: ‘last’
—--------------------------------------------------------------------
4- Ran 1 test in 0.000s
5- FAILED (errors=1)
- There is a lot of information here because there is a lot you might need to know when a test fails.
1- The single E tells us one unit test in the test case resulted in an error
2- Tells us that the particular unit test test_first_last_name is what failed in
NamesTestCase.
3- The typical Python error explaining what the error was. In this case, it was that
the function was looking for three arguments but only got two.
4- One unit test was run
5- The overall test case failed and one error occurred.
- When a test fails, don’t change the test. The new code that we introduced was the issue. The best option to fix our code is to make the middle name optional. Let’s modify get_formatted_name() so middle names are optional and then run the test case again. If it passes, we will move on to making sure the function handles middle names properly.
- To make middle names optional, we move the parameter middle to the end of the parameter list in the function definition and give it an empty default value. We also add an if statement that builds the full name properly, depending on whether or not a middle name is provided:
def get_formatted_name(first, last, middle = ‘’):
if middle: # When there is a middle, this is True
full_name = first + ‘ ‘ + middle + ‘ ‘ + last
else: # When no middle is given
full_name = first + ‘ ‘ + last
return full_name.title()
- If we run the test_name_function.py test again, the program works for the ‘Janis Joplin’ test. The result is:
.
—-------------------------------------------------------
Ran 1 test in 0.000s
OK
4.1.4 Adding a New Test
- Now we have to write a test for people with a middle name. We add another method to the class NamesTestsCase (the new code is in purple):
import unittest
from name_function import get_formatted_name
class NamesTestCase(unittest.TestCase):
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_middle_name(self):
‘’’ Do names like ‘Wolfgang Amadeus Mozart’ work?’’’
formatted_name = get_formatted_name(
‘wolfgang’, ‘mozart’, ‘amadeus’)
self.assertEqual(formatted_name, ‘Wolfgang Amadeus Mozart’)
unittest.main()
- The new method name must start with the word test_ so that it runs automatically when we run test_name_function.py. The name test_first_last_middle_name() is chosen so that if the test fails, we know right away what kinds of names are affected. Long names are ok because they need to be descriptive and they are called automatically.
- The result of running the test gives us:
.
—-------------------------------------------------------
Ran 2 tests in 0.000s
OK
- We now know that the function still works for names like ‘Janis Joplin’, and ‘Wolfgang Amadeus Mozart’ as well.
4.2 testing a Class
4.2.1 More “assert” methods
- We have already used an assert method from the unittest.TestCase class. Here is the code where we used it:
self.assertEqual(formatted_name, ‘Janis Joplin’)
- Here is a list of assert methods from the unittest module:
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
4.2.2 A Class to Test
- testing a class is similar to testing a function. Much of the work involves testing the behavior of the methods in the class, but there are some differences. We will start with a class that administers anonymous surveys, saved as survey.py:
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('- ' + response)
- This class starts with a survey question that you provide and includes an empty list to store responses. The class has methods to print the survey question, add a new response to the response list, and print all the responses stored in the list.
- To create an instance from this class, all you have to provide is a question. Once you have an instance representing a particular survey, you display the survey question with show_question(), store a response using store_response(), and show results with show_results().
- Here is a program that uses the class:
from survey import AnonymousSurvey
# Define a question, make a survey
question = "What language did you first learn to speak?"
my_survey = AnonymousSurvey(questions)
# 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()
- Here is a sample run of the program:
What language did you first learn to speak?
Enter ‘q’ at any time to quit.
Language: English
Language: Spanish
Language: English
Language: Mandarin
Language: q
Thank you to everyone who participated in the survey!
Survey results:
-English
-Spanish
-English
-Mandarin
- This class works for a simple anonymous survey, but let’s say we want to improve AnonymousSurvey and the module it’s in, survey. We could:
- allow each user to enter more than one response
- write a method to list only unique responses and to report how many times each response was given
- write another class to manage non anonymous surveys
- Creating these changes would risk affecting the current behavior of the class AnonymousSurvey. To ensure we don’t break existing behavior as we develop this module, we can write tests for the class.
4.2.3 testing the Class
- We will now write a test that verifies one aspect of how AnonymousSurvey behaves. The test will verify that a single response to the survey question is stored properly. We’ll use the assertIn() method to verify that the response is in the list of responses after it’s been stored:
import unittest
from survey import AnonymousSurvey
class TestAnonymousSurvey(unittest.TestCase):
""" Test for the class AnonymousSurvey """
def test_store_single_response(self):
""" Test 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)
unittest.main()
- The first test method will verify that when we store a response to the survey question, the response ends up in the survey’s list of responses.
- To test the behavior of the class, we need to make an instance of the class. The first three lines (not including the comment) of the test_store_single_response() method:
- Creates a variable called question
- Creates an instance with the question variable as our question
- Stores a single response, English, using the store_response() method
- The assert.In method is then used to verify that the response was stored correctly by asserting that English is in the list my_survey.responses.
- When we run test_survey.py, the test passes:
.
—----------------------------------------------
Ran 1 test in 0.0001s
OK
- This is a good start, but only works for one response. Let’s verify that three responses can be stored correctly. We need to add another method to TestAnonymousSurvey:
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']
for response in responses:
my_survey.store_response(response)
for response in responses:
self.assertIn(response, my_survey.responses)
- Here, we create a survey object just like we did in test_store_single_response(). We define a list containing three different responses, and then we call store_response() for each of these responses. Once the responses have been stored, we write another loop and assert that each response is now in my_survey.responses.
- When we run test_survey.py again, both tests (for a single response and for three responses) pass:
..
—--------------------------------
Ran 2 tests in 0.000s
OK
- In test_survey.py we created a new instance of AnonymousSurvey in each test method, and we created new responses in each method. The unittest.TestCase class has a setUp() method that allows you to create these objects once and then use them in each of your test methods. When you include a setUp() method in a TestCase class, Python runs the setUp() method before running each method starting with test_. Any objects created in the setUp() method are then available in each test method you write.
- We will use setUp() to create a survey instance and a set of responses that can be used in test_store_single_response() and test_store_three_responses():
import unittest
from survey import AnonymousSurvey
class TestAnonymousSurvey(unittest.TestCase):
""" Test for the class AnonymousSurvey """
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 = AnonymouseSurvey(question)
self.responses = ['English', 'Spanish', 'Mandarin']
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)
def test_store_three_responses(self):
""" Test that three individual responses are stored properly """
for response in responses:
self.my_survey.store_response(response)
for response in self.responses:
self.assertIn(response, self.my_survey.responses)
unittest.main()
- The method setUp() does two things:
- It creates a survey instance
- It creates a list of responses
- Each of these is prefixed by self, so they can be used anywhere in the class. This makes the two test methods simpler, because neither one has to make a survey instance or a response. The method test_store_single_response() verifies that the first response in self.responses, which is self.responses[0], can be stored correctly, and test_store_three_responses() verifies that all three responses in self.responses can be stored correctly.
4.3 Conclusion
- When a test case is running, Python prints one character for each unit test as it is completed. A passing test prints a dot, a test that results in an error prints an E, and a test that results in a failed assertion prints as F.
- testing is an important topic that many beginners don’t learn. As your projects become more complex, you should test critical behaviors of your functions and classes.
Chapter 4 Assignments
A4.1 City, Country
- Write a function that accepts two parameters: a city name and a country name. The function should return a single string of the form City, Country, such as Santigo, Chile. Store the function in a module called city_functions.py.
- Create a file called test_cities.py that tests the function you just wrote (don’t forget to import unittest and the function you want to test). Write a method called test_city_country() to verify that calling your function with values such as ‘santiago’ and ‘chile’ results in the correct string.
- Run test_cities.py, and make sure test_city_country() passes.
A4.2 Population
- Modify your function in A4.1 so it requires a third parameter, population.
- It should now return a single string of the form City, Country - population xxx, such as Santiago, Chile - population 5000000
- Run test_cities.py again. Make sure test_city_country() fails this time.
- Modify the function so the population parameter is optional. Run test_cities.py again, and make sure test_city_country() passes again.
- Write a second test called test_city_country_population() that verifies you can call your function with the values ‘santiago’, ‘chile’, and ‘population=5000000’.
- Run test_cities.py again, and make sure this new test passes.
A4.3 Employee
- Write a class called Employee. The __init__() method should take in a first name, a last name, and an annual salary, and store each of these as attributes. Write a method called give_raise() that adds $5000 to the annual salary by default but also accepts a different raise amount.
- Write a test case for Employee. Write two test methods, test_give_default_raise() and test_give_custom_raise(). Use the setUp() method so you don’t have to create a new employee instance in each test method.
- Run your test case, and make sure both tests pass
(more potential ASSIGNMENTS)