Responding to a failed test
- when a test fails, don’t change the test
- need to change the code that calls your function
- get_formatted_name() used to require two parameters, now three
- make the middle name optional by moving the parameter middle to the end of the parameter list in the function definition and give it an empty default value
- add an if test that builds the full name properly, depending on if a middle name is provided
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()
- run test again for Janis Joplin
$ pytest
======================== test session starts ===================
==========
–snip–
test_name_function.py . [100%]
======================== 1 passed in 0.00s ===================
==========
- test passed
Adding new tests
- add a second test for people with middle names by adding another test function to test_name_function.py
test_name_function.py
from name_function import get_formatted_name
.
def test_first_last_name():
….–snip–
.
def test_first_last_middle_name():
….”””Do names like ‘Wolfgang Amadeus Mozart’ work?”””
….formatted_name = get_formatted_name(
……..’wolfgang’, ‘mozart’, ‘amadeus’)
….assert formatted_name == ‘Wolfgang Amadeus Mozart’
- new function test_first_last_middle_name()
- function name must start with test_ so the function runs automatically when we run pytest
- choose an obvious name so if the test fails we know right away what names are affected
- call get_formatted_name() with a first, last, and middle name
- then make an assertion that the returned full name matches the full name (first, middle, and last)
- run pytest, both tests pass
$ pytest
======================== test session starts ===================
==========
–snip–
collected 2 items
.
test_name_function.py .. [100%]
======================== 2 passed in 0.01s ===================
==========
- two dots indicate two tests passed
- we know the function will work for names like Janis Joplin and Wolfgang Amadeus Mozart
Testing a class
- previously wrote tests for a single function
- now we’ll write tests for a class
A variety of assertions
- so far, only seen one assertion: a claim that a string has a certain value
- for tests, can make any claim that can be expressed as a conditional statement
- if the condition is True, your assumption about how part of your program behaves will be confirmed
- if the condition is False, the test will fail
Table 11-1: commonly used assertion statements in tests
assertion | claim
assert a == b – assert that two values are equal
assert a != b – assert that two values are not equal
assert a – assert that a evaluates to True
assert not a – assert that a evaluates to False
assert element in list – assert that an element is in a list
assert element not in list – assert that an element is not in a list
- these are a few examples, anything that can be expressed as a conditional statement can be included in a test
A class to test
- testing a class is similar to testing a function, but there are some differences
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_results(self):
……..”””Show all the responses that have been given.”””
……..print(“Survey results:”)
……..for response in self.responses:
……..print(f”- {response}”)
- class starts with a survey question an includes an empty list to store responses
- class has methods to print the survey question
- add a new response to the response list
- print all the responses stored in the list
- to create an instance from this class, provide a question
- display the survey question with show_question(), store a response using sotore_responsive(), and show results with show_results()
- show AnonymousSurvey class works by writing a program that uses the class
language_survey.py
from survey import AnonymousSurvey
.
# Define a question, and make a survey.
question = “What language did you first learn to speak?”
language_survey = AnonymousSurvey(question)
.
# Show the question, and store responses to the question.
language_survey.show_question()
print(“Enter ‘q’ at any time to quit.\n”)
while True:
….response = input(“Language: “)
….if response == ‘q’:
……..break
….language_survey.store_response(response)
.
# Show the survey results.
print(“\nThank you to everyone who participated in the survey!”)
language_survey.show_results()
- program defines a question (“What language did you first learn to speak?”) and creates an AnonymousSurvey object with that question
- program calls show_question() to display the question and prompts for responses
- responses are stored as they’re received
- after all responses have been entered the user inputs q to quit
- show_results() prints the results
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 if we want to improve AnonymousSurvey and the module it’s in, survey, we could allow each user to enter more than one response
- could write a method to list only unique responses and to report how many times each response was given
- or we could write another class to manage non-anonymous surveys
- write tests for this class to ensure we don’t break how single responses are handled
Testing the AnonymousSurvey class
- let’s write a test that verifies a single response to the survey question is stored properly
test_survey.py
from survey import AnonymousSurvey
.
def test_store_single_response():
….”””Test that a single response is stored properly.”””
….question = “What language did you first learn to speak?”
….language_survey = AnonymousSurvey(question)
….language_survey = AnonymousSurvey(question)
….language_survey.store_response(‘English’)
….assert ‘English’ in language_survey.responses
- start by importing the class we want to test, AnonymouSurvey
- first test function will verify a stored response will end up in the survey’s list of responses
- test_store_single_response() is a good descriptive name
- need to make an instance of the class to test its behavior
- create an instance called language_survey with the question “What language did you first learn to speak?”
- store a single response, English, using the store_response() method
- verify response was stored correctly by asserting that English is in the list language_survey.responses
- run test we wrote for AnonymouSurvey
$ pytest
======================== test session starts ===================
==========
–snip–
test_survey . [100%]
======================== 1 passed in 0.00s ===================
==========
- let’s verify that three responses can be stored correctly by adding another method to TestAnonymousSurvey
from survey import AnonymousSurvey
def test_store_single_response():
….–snip–
.
def test_store_three_responses():
….”””Test that three individual responses are stored properly.”””
….question = “What language did you first learn to speak?”
….language_survey = AnonymousSurvey(question)
….responses = [‘English’, ‘Spanish’, ‘Mandarin’]
….for response in responses:
……..language_survey.store_response(response)
.
for response in responses:
……..assert response in language_survey.responses
- call new function test_store_three_responses()
- create survey object
- define a list containing three different responses
- call store_response() for each response
- write another loop and assert that each response is now in lagnauge_survey.responses
- run test file again, both tests pass
$ pytest
======================== test session starts ===================
==========
–snip–
test_survey .. [100%]
======================== 1 passed in 0.00s ===================
==========
- this works, but tests are repetitive, so we’ll use another feature of pytest to make them more efficient
Using fixtures
- in test_survey.py, we created a new instance of AnonymousSurvey in each test function
- fine for short examples, but in a real-world project with hundreds of tests, this would be difficult
- fixture – sets up a test environment
- create a resource that’s used by more than one test
- create a fixture in pytest by writing a function with the decorator @pytest.fixture
- decorator – a directive placed just before a function definition
- Python applies this directive to the function before it runs to alter how the function code behaves
- use decorators from third-party packages before learning to write them yourself
- we’ll use a picture to create a single survey instance that can be used in both test functions in test_survey.py
import pytest
from survey import AnonymousSurvey
.
@pytest.fixutre
def lagnuage_survey():
….”””A survey that will be available to all test functions.”””
….question = “What language did you first learn to speak?”
….language_survey – AnonymouSurvey(question)
….return language_survey
.
def test_store_single_response(language_survey):
….”””Test that a single response is stored properly.”””
….language_survey.store_response(‘English’)
….assert ‘English’ in language_survey.responses
.
def test_store_three_responses(language_survey):
….”””Test that three individual responses are stored properly.”””
….responses = [‘English’, ‘Spanish’, ‘Mandarin’]
….for response in responses:
……..language_survey.store_response(response)
.
….for response in responses:
……..assert response in language_survey.responses
- need to import pytest now, because we’re using a decorator that’s defined in pytest
- we apply the @pytest.fixture decorator to the new function language_survey(), which builds an AnonymousSurvey object and returns the new survey
- notice that the definitions of both test functions have changed
- each test function has a parameter called language_survey
- whena. parameter in a test function matches the name of a function with the @pytest.fixture decorator, the fixture will be run automatically and the return value will be passed to the test function
- function language_survey() supplies both test_store_single_response and test_store_multiple_responses() with a language_survey instance
- no new code in either function of the test functions, but two lines have been removed from each function: the line that defined a question and the line that created an AnonymousSurvey object
- when we run the test file again, both tests still pass
- this structure looks complicated, with abstract code
- don’t need to use fixtures right away
- start with writing tests with repetitive code instead of no tests at all
- when you want to write a fixture, write a function that generates the resource that’s used by multiple test functions
- add the @pytest.fixture decorator the the new function, and add the name of this function as a parameter for each test function that uses this resource
- your tests will be shorter and easier to maintain
Summary
- learned to write tests for functions and classes using tools in the pytest module
- learned to write test functions for functions and classes
- saw how fixtures can be used to efficiently create resources that can be used in multiple test functions in a test file
- test critical behaviors for projects that involve significant development effort
- better to test along the way instead of breaking the functionality of your code and needing to respond to a bug report from a user
- testing highlights competency and increased confidence working with you as a programmer
- as a contributor to a project, write tests for any new behavior you introduce
End of study session.