Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
May 30, 2022 04:26 pm GMT

Testing Your Python Code

When you write a function or a class, you can also write tests for that code. Testing proves that your code works as its supposed to in response to all the input types its designed to receive. When you write tests, you can be confident that your code will work correctly as more people begin to use your programs. Youll also be able to test new code as you add it to make sure your changes dont break your programs existing behavior. Every programmer makes mistakes, so every programmer must test their code often, catching problems before users encounter them.

Testing a Function

To learn about testing, we need code to test. Heres a simple function that takes in a first and last name, and returns a neatly formatted full name:

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

The function get_formatted_name() combines the first and last name with a space in between to complete a full name, and then capitalizes and returns the full name. To check that get_formatted_name() works, lets 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_nameprint("Enter 'q' at any time to quit.")while True:    first = input("
Please 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"Neatly formatted name: {formatted_name}.")

This program imports get_formatted_name() from name_function. py. The user can enter a series of first and last names, and see the formatted full names that are generated:

Enter 'q' at any time to quit.Please give me a first name: ahmedPlease give me a last name: gouda        Neatly formatted name: Ahmed Gouda.Please give me a first name: mohammedPlease give me a last name: ali        Neatly formatted name: Mohammed Ali.Please give me a first name: q

We can see that the names generated here are correct. But lets say we want to modify get_formatted_name() so it can also handle middle names. As we do so, we want to make sure we dont break the way the function handles names that have only a first and last name. We could test our code by running names. py and entering a name like Mohammed Ali every time we modify get_formatted_name(), but that would become tedious. Fortunately, Python provides an efficient way to automate the testing of a functions output. If we automate the testing of get_formatted_name(), we can always be confident that the function will work when given the kinds of names weve written tests for.

Unit Tests and Test Cases

The module unittest from the Python standard library provides tools for testing your code.

  • A unit test verifies that one specific aspect of a functions behavior is correct.
  • A test case is a collection of unit tests that together prove that a function behaves as its 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. Achieving full coverage on a large project can be daunting. Its often good enough to write tests for your codes critical behaviors and then aim for full coverage only if the project starts to see widespread use.

A Passing Test

The syntax for setting up a test case takes some getting used to, but once youve set up the test case its straightforward to add more unit tests for your functions. 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 functions behavior.

Heres a test case with one method that verifies that the function get_formatted_name() works correctly when given a first and last name:

import unittestfrom name_function import get_formatted_nameclass NamesTestCase(unittest.TestCase):    """Tests for 'name_function.py'."""    def test_first_last_name(self):        """Do names like 'Mohammed Ali' work?"""        formatted_name = get_formatted_name('Mohammed', 'Ali')        self.assertEqual(formatted_name, 'Mohammed Ali')if __name__ == '__main__':    unittest.main()

First, we import unittest and the function we want to test, get_formatted_name(). At class NamesTestCase(unittest.TestCase): we create a class called NamesTestCase, which will contain a series of unit tests for get_formatted_name().

You can name the class anything you want, but its best to call it something related to the function youre about to test and to use 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.

NamesTestCase contains a single method that tests one aspect of get_formatted_name(). We call this method test_first_last_name() because were verifying that names with only a first and last name are formatted correctly. 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. In this example we call get_formatted_name() with the arguments 'Mohammed' and 'Ali', and assign the result to formatted_name formatted_name = get_formatted_name('Mohammed', 'Ali').

At self.assertEqual(formatted_name, 'Mohammed Ali') we use one of unittests most useful features: an assert method. Assert methods verify that a result you received matches the result you expected to receive. In this case, because we know get_formatted_name() is supposed to return a capitalized, properly spaced full name, we expect the value of formatted_name to be Mohammed Ali. To check if this is true, we use unittests assertEqual() method and pass it formatted_name and 'Mohammed Ali'.

The line self.assertEqual(formatted_name, 'Mohammed Ali') says, Compare the value in formatted_name to the string 'Mohammed Ali'. If they are equal as expected, fine. But if they dont match, let me know!

Were going to run this file directly, but its important to note that many testing frameworks import your test files before running them. When a file is imported, the interpreter executes the file as its being imported. The if block

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

looks at a special variable, __name__, which is set when the program is executed. If this file is being run as the main program, the value of __name__ is set to __main__. In this case, we call unittest.main(), which runs the test case. When a testing framework imports this file, the value of __name__ wont be __main__ and this block will not be executed.

When we run test_name_function. py, we get the following output:

.----------------------------------------------------------------------Ran 1 test in 0.000sOK

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 get_formatted_name() will always work for names that have a first and last name unless we modify the function. When we modify get_formatted_name(), we can run this test again. If the test case passes, we know the function will still work for names like Mohammed Ali.

A Failing Test

What does a failing test look like? Lets modify get_formatted_name() so it can handle middle names, but well do so in a way that breaks the function for names with just a first and last name, like Mohammed Ali.

Heres a new version of get_formatted_name() that requires a middle name argument:

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

This version should work for people with middle names, but when we test it, we see that weve broken the function for people with just a first and last name. This time, running the file test_name_function. py gives this output:

E======================================================================ERROR: test_first_last_name (__main__.NamesTestCase)----------------------------------------------------------------------Traceback (most recent call last):    File "test_name_function.py", line 8, in test_first_last_name        formatted_name = get_formatted_name('Mohammed', 'Ali')TypeError: get_formatted_name() missing 1 required positional argument: 'last----------------------------------------------------------------------Ran 1 test in 0.000sFAILED (errors=1)

Theres a lot of information here because theres a lot you might need to know when a test fails.

The first item in the output is a single E, which tells us one unit test in the test case resulted in an error.

Next, we see that test_first_last_name() in NamesTestCase caused an error. Knowing which test failed is critical when your test case contains many unit tests.

Then, we see a standard traceback, which reports that the function call get_formatted_name('Mohammed', 'Ali') no longer works because its missing a required positional argument.

We also see that one unit test was run. Finally, we see an additional message that the overall test case failed and that one error occurred when running the test case. This information appears at the end of the output so you see it right away; you dont need to scroll up through a long output listing to find out how many tests failed.

Responding to a Failed Test

What do you do when a test fails? Assuming youre checking the right conditions, a passing test means the function is behaving correctly and a failing test means theres an error in the new code you wrote. So when a test fails, dont change the test. Instead, fix the code that caused the test to fail. Examine the changes you just made to the function, and figure out how those changes broke the desired behavior.

In this case get_formatted_name() used to require only two parameters: a first name and a last name. Now it requires a first name, middle name, and last name. The addition of that mandatory middle name parameter broke the desired behavior of get_formatted_name(). The best option here is to make the middle name optional. Once we do, our test for names like Mohammed Ali should pass again, and we should be able to accept middle names as well.

Lets modify get_formatted_name() so middle names are optional and then run the test case again. If it passes, well 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 test that builds the full name properly, depending on whether or not a middle name is provided:

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

In this new version of get_formatted_name(), the middle name is optional. If a middle name is passed to the function, the full name will contain a first, middle, and last name. Otherwise, the full name will consist of just a first and last name. Now the function should work for both kinds of names.

To find out if the function still works for names like Mohammed Ali, lets run test_name_function. py again:

.----------------------------------------------------------------------Ran 1 test in 0.000sOK

The test case passes now. This is ideal; it means the function works for names like Mohammed Ali again without us having to test the function manually. Fixing our function was easy because the failed test helped us identify the new code that broke existing behavior.

Adding New Tests

Now that we know get_formatted_name() works for simple names again, lets write a second test for people who include a middle name. We do this by adding another method to the class NamesTestCase:

import unittestfrom name_function import get_formatted_nameclass NamesTestCase(unittest.TestCase):    """Tests for 'name_function.py'."""    def test_first_last_name(self):        """Do names like 'Mohammed Ali' work?"""        formatted_name = get_formatted_name('Mohammed', 'Ali')        self.assertEqual(formatted_name, 'Mohammed Ali')    def test_first_last_middle_name(self):        """Do names like 'Ahmed Mohammed Gouda' work?"""        formatted_name = get_formatted_name('Ahmed', 'Gouda', 'Mohammed')        self.assertEqual(formatted_name, 'Ahmed Mohammed Gouda')if __name__ == '__main__':    unittest.main()

We name this new method test_first_last_middle_name(). The method name must start with test_ so the method runs automatically when we run test_name_function. py. We name the method to make it clear which behavior of get_formatted_name() were testing. As a result, if the test fails, we know right away what kinds of names are affected. Its fine to have long method names in your TestCase classes. They need to be descriptive so you can make sense of the output when your tests fail, and because Python calls them automatically, youll never have to write code that calls these methods.

To test the function, we call get_formatted_name() with a first, last, and middle name formatted_name = get_formatted_name('Ahmed', 'Gouda', 'Mohammed'), and then we use assertEqual() to check that the returned full name matches the full name (first, middle, and last) that we expect. When we run test_name_function. py again, both tests pass:

..----------------------------------------------------------------------Ran 2 tests in 0.000sOK

Great! We now know that the function still works for names like Mohammed
Ali, and we can be confident that it will work for names like Ahmed Mohammed Gouda as well.

Testing a Class

We've just wrote tests for a single function. Now youll write tests for a class. Youll use classes in many of your own programs, so its helpful to be able to prove that your classes work correctly. If you have passing tests for a class youre working on, you can be confident that improvements you make to the class wont accidentally break its current behavior.

A Variety of Assert Methods

Python provides a number of assert methods in the unittest.TestCase class. As mentioned earlier, assert methods test whether a condition you believe is true at a specific point in your code is indeed true. If the condition is true as expected, your assumption about how that part of your program behaves is confirmed; you can be confident that no errors exist. If the condition you assume is true is actually not true, Python raises an exception.

Below table describes six commonly used assert methods. With these methods you can verify that returned values equal or dont equal expected values, that values are True or False, and that values are in or not in a given list. You can use these methods only in a class that inherits from unittest.TestCase, so lets look at how we can use one of these methods in the context of testing an actual class.

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
assertIn(item, list)Verify that item is in list
assertNotIn(item, list)Verify that item is not in list

A Class to Test

Testing a class is similar to testing a functionmuch of your work involves testing the behavior of the methods in the class. But there are a few differences, so lets write a class to test. Consider a class that helps administer anonymous surveys:

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}")

This class starts with a survey question that you provide def __init__(self, question): and includes an empty list to store responses. The class has methods to print the survey question def show_question(self):, add a new response to the response list def store_response(self, new_response):, and print all the responses stored in the list def show_results(self):.

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

To show that the AnonymousSurvey class works, lets write a program that uses the class:

This program defines a question ("What language did you first learn to speak?") and creates an AnonymousSurvey object with that question. The program calls show_question() to display the question and then prompts for responses. Each response is stored as it is received. When all responses have been entered (the user inputs q to quit), show_results() prints the survey results:

What language did you first learn to speak?Enter 'q' at any time to quit.Language: ArabicLanguage: EnglishLanguage: EnglishLanguage: q  Thank you to everyone who participated in the survey!Survey results:- Arabic- English- English

This class works for a simple anonymous survey. But lets say we want to improve AnonymousSurvey and the module its in, survey. We could allow each user to enter more than one response. We could write a method to list only unique responses and to report how many times each response was given. We could write another class to manage non-anonymous surveys.

Implementing such changes would risk affecting the current behavior of the class AnonymousSurvey. For example, its possible that while trying to allow each user to enter multiple responses, we could accidentally change how single responses are handled. To ensure we dont break existing behavior as we develop this module, we can write tests for the class.

Testing the AnonymousSurvey Class

Lets write a test that verifies one aspect of the way AnonymousSurvey behaves. Well write a test to verify that a single response to the survey question is stored properly. Well use the assertIn() method to verify that the response is in the list of responses after its been stored:

import unittestfrom survey import AnonymousSurveyclass 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)if __name__ == '__main__':    unittest.main()

We start by importing the unittest module and the class we want to test, AnonymousSurvey. We call our test case TestAnonymousSurvey, which again inherits from unittest.TestCase. The first test method will verify that when we store a response to the survey question, the response ends up in the surveys list of responses. A good descriptive name for this method is test_store_single_response(). If this test fails, well know from the method name shown in the output of the failing test that there was a problem storing a single response to the survey.

To test the behavior of a class, we need to make an instance of the class. At my_survey = AnonymousSurvey(question) we create an instance called my_survey with the question "What language did you first learn to speak?" We store a single response, English, using the store_response() method. Then we verify that the response was stored correctly by asserting that English is in the list my_survey. responses self.assertIn('English', my_survey.responses).

When we run test_survey. py, the test passes:

.----------------------------------------------------------------------Ran 1 test in 0.000sOK

This is good, but a survey is useful only if it generates more than one response. Lets verify that three responses can be stored correctly. To do this, we add another method to TestAnonymousSurvey:

import unittestfrom survey import AnonymousSurveyclass 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 = ['Arabic', 'English', 'Spanish']        for response in responses:            my_survey.store_response(response)        for response in responses:            self.assertIn(response, my_survey.responses)if __name__ == '__main__':    unittest.main()

We call the new method test_store_three_responses(). We create a survey object just like we did in test_store_single_response(). We define a list containing three different responses responses = ['Arabic', 'English', 'Spanish'], 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 for response in responses:.

When we run test_survey. py again, both tests (for a single response and for three responses) pass:

..----------------------------------------------------------------------Ran 2 tests in 0.000sOK

This works perfectly. However, these tests are a bit repetitive, so well use another feature of unittest to make them more efficient.

The setUp() Method

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.

Lets 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 unittestfrom survey import AnonymousSurveyclass 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 = ['Arabic', 'English', 'Spanish']    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()

The method setUp() does two things: it creates a survey instance self.my_survey = AnonymousSurvey(question), and it creates a list of responses self.responses = ['Arabic', 'English', 'Spanish']. 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.responsesself.responses[0]can be stored correctly, and test_store _three_responses() verifies that all three responses in self.responses can be stored correctly.

When we run test_survey. py again, both tests still pass. These tests would be particularly useful when trying to expand AnonymousSurvey to handle multiple responses for each person. After modifying the code to accept multiple responses, you could run these tests and make sure you havent affected the ability to store a single response or a series of individual responses.

When youre testing your own classes, the setUp() method can make your test methods easier to write. You make one set of instances and attributes in setUp() and then use these instances in all your test methods. This is much easier than making a new set of instances and attributes in each test method.

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 an F. This is why youll see a different number of dots and characters on the first line of output when you run your test cases. If a test case takes a long time to run because it contains many unit tests, you can watch these results to get a sense of how many tests are passing.


Original Link: https://dev.to/ahmedgouda/testing-your-python-code-17dd

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To