跳到主要内容

Testing Your Code

When you write a function or a class, you can also write tests for that code. Testing proves that your code works as it's supposed to in response to all the input types it's designed to receive.

Testing a Function

A Function to Test

Let's start with a simple function that takes in a first and last name and returns a neatly formatted full name:

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

Here's a program that uses this function:

# names.py
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}.")

Unit Tests and Test Cases

Python's unittest module provides tools for testing your code. A unit test verifies 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 Passing Test

Here's a test case for get_formatted_name():

# 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:

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

OK

Key Components of a Test

  1. Import unittest and the function to test
  2. Create a class that inherits from unittest.TestCase
  3. Write test methods that start with test_
  4. Use assertion methods to verify expected behavior
  5. Include unittest.main() to run the tests

A Failing Test

Let's modify our function to handle middle names:

# name_function.py (modified)
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()

Now let's add a test for middle names:

# test_name_function.py (with failing test)
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')

def test_first_middle_last_name(self):
"""Do names like 'Wolfgang Amadeus Mozart' work?"""
formatted_name = get_formatted_name('wolfgang', 'mozart', 'amadeus')
self.assertEqual(formatted_name, 'Wolfgang Amadeus Mozart')

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

Testing a Class

A Class to Test

Here's a class that models a survey:

# 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(f"- {response}")

Testing the AnonymousSurvey Class

# test_survey.py
import unittest
from survey import AnonymousSurvey

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

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)

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

The setUp() Method

The unittest.TestCase class has a setUp() method that allows you to create objects once and use them in each of your test methods:

# test_survey.py (improved)
import unittest
from survey import AnonymousSurvey

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

Assert Methods

The unittest module provides many assert methods to test different conditions:

MethodUse
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
assertIs(a, b)Verify that a is b
assertIsNot(a, b)Verify that a is not b
assertIsNone(x)Verify that x is None
assertIsNotNone(x)Verify that x is not None
assertIn(item, list)Verify that item is in list
assertNotIn(item, list)Verify that item is not in list
assertIsInstance(a, b)Verify that isinstance(a, b)
assertNotIsInstance(a, b)Verify that not isinstance(a, b)

Testing Guidelines

When to Write Tests

When you're starting out, write tests for the most critical behaviors of your functions and classes. As you become more experienced, you'll write them more regularly.

Testing Best Practices

  1. Test one thing at a time: Each test method should test one specific behavior
  2. Use descriptive test names: Your test method names should clearly indicate what they're testing
  3. Keep tests simple: Tests should be easy to understand and maintain
  4. Test edge cases: Don't just test typical inputs; test unusual cases too
  5. Use setUp() for common setup: If multiple tests need the same setup, use the setUp() method

What to Test

Good candidates for testing include:

  • Functions with complex logic
  • Functions that handle user input
  • Functions that process data from external sources
  • Edge cases and error conditions
  • Critical business logic

Example: Testing Edge Cases

# calculator.py
def divide(a, b):
"""Divide a by b."""
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b

# test_calculator.py
import unittest
from calculator import divide

class TestCalculator(unittest.TestCase):
"""Tests for calculator functions."""

def test_divide_positive_numbers(self):
"""Test dividing positive numbers."""
result = divide(10, 2)
self.assertEqual(result, 5)

def test_divide_negative_numbers(self):
"""Test dividing negative numbers."""
result = divide(-10, 2)
self.assertEqual(result, -5)

def test_divide_by_zero(self):
"""Test that dividing by zero raises ValueError."""
with self.assertRaises(ValueError):
divide(10, 0)

def test_divide_floats(self):
"""Test dividing floating point numbers."""
result = divide(7.5, 2.5)
self.assertEqual(result, 3.0)

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

Running Tests

You can run your tests in several ways:

# Run a specific test file
python test_name_function.py

# Run all tests in a directory
python -m unittest discover

# Run tests with more verbose output
python -m unittest -v test_name_function.py

Summary

In this chapter you learned how to:

  • Write simple tests using Python's unittest module
  • Use assert methods to verify that your functions and classes work correctly
  • Use the setUp() method to create objects that can be used in multiple test methods
  • Test both functions and classes
  • Understand when and what to test

Testing your code is essential for building reliable software. When you write tests, you can be confident that your code will work correctly even as you make changes and improvements to your programs. Testing gives you the freedom to improve your code without worrying about accidentally breaking existing functionality.