Skip to content

Learning Python: Day 39

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.

Tags:

Leave a Reply

Your email address will not be published. Required fields are marked *