The introduction of unit testing to debug code during Python programming

  • 2020-05-05 11:24:05
  • OfStack

One of the most common puzzles for novice developers is the subject of testing. They feel vaguely that "unit testing" is fine, and that they should do it, too. But they don't know what it really means. If this sounds like you, don't be afraid! In this article, I'll explain what unit testing is, why it's useful, and how to unit test Python's code.

What is a test?

Before we discuss why testing is useful and how to do it, let's take a few minutes to define exactly what a "unit test" is. In general programming terms, "testing" means writing code that can be called (independent of your actual application) to help you determine if there are errors in your program. This does not prove that your code is correct (it is the only possibility in very limited cases). It simply reports whether the situation the tester thinks was handled correctly.

Note: when I use "tests" once, I mean "automated tests," meaning that the tests are run on a machine. "Manual testing," in which a person runs a program and interacts with it to find vulnerabilities, is a separate concept.

What can the test check for? Grammatical errors are accidental misuse of language, such as
 


my_list..append(foo)

The extra ". "in the back. Logic errors are caused when the algorithm (which can be thought of as "the way to solve the problem") is not correct. Maybe the programmer forgot that Python is "zero-indexed" and tried to write
 


print(my_string[len(my_string)])

(this causes IndexError) to print out the last character in a string. Larger, more systematic errors can also be detected. For example, when a user enters a number greater than 100, or hangs a web site when the search is unavailable, the program keeps crashing.

All of these errors can be detected by careful testing of the code. Unit testing, Unit, Unit, testing, Unit, Unit, Unit testing, Unit testing, Unit testing A unit can be an entire module, a single class or function, or any code in between. However, it is important that the test code be isolated from the rest of the code that we are not testing (because the rest of the code itself has errors that can confuse the test results). Consider the following example:
 


def is_prime(number):
  """Return True if *number* is prime."""
  for element in range(number):
    if number % element == 0:
      return False
 
  return True
 
def print_next_prime(number):
  """Print the closest prime number larger than *number*."""
  index = number
  while True:
    index += 1
    if is_prime(index):
      print(index)

You have two functions, is_prime and print_next_prime. If you want to test print_next_prime, we need to make sure is_prime is correct because this function is called in print_next_prime. In this case, the print_next_prime function is one cell and the is_prime function is another cell. Since unit tests only test one unit at a time, we need to carefully consider how we can accurately test. (more on how to implement these tests later).

So what should the test code look like? If the previous example exists in a file called primes.py, we can write the test code in a file called test_primes.py. Here is the most basic content in test_primes.py, such as the following test sample:
 


import unittest
from primes import is_prime
 
class PrimesTestCase(unittest.TestCase):
  """Tests for `primes.py`."""
 
  def test_is_five_prime(self):
    """Is five successfully determined to be prime?"""
    self.assertTrue(is_prime(5))
 
if __name__ == '__main__':
  unittest.main()

This file is passed through an test case :? test_is_five_prime. Creates a unit test. Through Python embedded with a test framework unittest. When unittest.main () is called, any member function named beginning with test, a derived class of unittest.TestCase, is run and checked for assertions. If we run the test by typing python test_primes.py, we can see the output of the unittest framework on the console:
 


$ python test_primes.py
E
======================================================================
ERROR: test_is_five_prime (__main__.PrimesTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_primes.py", line 8, in test_is_five_prime
  self.assertTrue(is_prime(5))
File "/home/jknupp/code/github_code/blug_private/primes.py", line 4, in is_prime
  if number % element == 0:
ZeroDivisionError: integer division or modulo by zero
 
----------------------------------------------------------------------
Ran 1 test in 0.000s

The separate "E" represents the result of our unit test (if it succeeds, a ". "is printed). We can see that our test failed, the line of code that caused the failure, and any exception information that was thrown.

Why test ?

Before we continue with that example, it's important to ask the question, "why is testing valuable to me?" This is a fair question and one often asked by those unfamiliar with code testing. After all, testing takes a certain amount of time, and we can spend that time coding, so why test instead of doing what does most productively?

There are many effective answers to this question, and I have listed the following:

The       test ensures that your code works under a given set of conditions

Testing ensures correctness under a range of conditions. Syntax errors are almost certainly detected by tests, and the basic logic of a code unit can be detected by tests to ensure correctness under certain conditions. Again, it's not about proving that the code is correct under any conditions. We simply aim for a fairly complete set of possible conditions (for example, you can write a test to monitor when you call my_addition_function(3, 'refrigerator), but you don't have to detect all possible strings for each parameter)

The       test allows people to ensure that changes to the code do not break existing functionality

This is especially useful when refactoring code. If you don't have the tests in place, you can't be sure that your code changes don't break things that worked before. If you want to change or rewrite your code and hope not to break anything, proper unit testing is necessary.

      tests force people to think about the code under unusual conditions, which can reveal logic errors

Writing tests forces you to think about the problems your code might encounter under unusual conditions. In the above example, the my_addition_function function can add two Numbers. A simple test to test basic correctness calls my_addition_function (2,2) and asserts that the result is 4. However, further tests may test whether this function performs floating-point arithmetic correctly by calling my_addition_function (2.0,2.0). Defensive coding principles suggest that your code should be able to fail in the case of illegal input, so when testing, an exception should be thrown when a string type is passed into a function as an argument.

      good testing requires modular, decoupled code, which is a hallmark of good system design

The whole idea of unit testing is to make it easier through loose coupling of code. If your application code calls the database directly, for example, testing your application's logic depends on a valid database connection, and the test data exists in the database. On the other hand, code that isolates external resources is more easily replaced by mock objects during testing. Test-capable applications designed out of necessity ended up being modular and loosely coupled.

Anatomy of unit tests

Continuing with the previous example, we'll see how to write and organize unit tests. Recall that primes.py contains the following code:


def is_prime(number):
  """Return True if *number* is prime."""
  for element in range(number):
    if number % element == 0:
      return False
 
  return True
 
def print_next_prime(number):
  """Print the closest prime number larger than *number*."""
  index = number
  while True:
    index += 1
    if is_prime(index):
      print(index)

Meanwhile, the file test_primes.py contains the following code:
 


import unittest
from primes import is_prime
 
class PrimesTestCase(unittest.TestCase):
  """Tests for `primes.py`."""
 
  def test_is_five_prime(self):
    """Is five successfully determined to be prime?"""
    self.assertTrue(is_prime(5))
 
if __name__ == '__main__':
  unittest.main()

makes assertions

unittest is part of the Python standard library and a good place to start our "unit test journey." A unit test includes one or more assertions (statements that claim some properties of the code under test to be true). When you go to school, the word "assertion" literally means "statement of fact." The same goes for assertions in unit tests.

self.assertTrue is more self-explanatory. It can declare that the calculated result of the passed parameter is true. The unittest.TestCase class contains many assertion methods, so be sure to check the list and choose the appropriate method to test. If assertTrue is used in every test, an anti-pattern should be considered because it increases the cognitive burden on the reader in the test. The correct use of assertions should be to enable the test to specify exactly what is being asserted (for example, obviously ? , you only need to glance at the method name of assertIsInstance to see that it is specifying its parameters.

Each test should test a separate, feature-specific piece of code and should be named accordingly. Research on unit test discovery mechanisms (mainly in Python2.7 + and 3.2+ versions) suggests that test methods should be named after the test_ prefix. This is configurable, but its purpose is to identify test methods and non-test utility methods. If we change the name of test_is_five_prime to is_five_prime, running test_primes.py in python will output the following information :
 


$ python test_primes.py
 
----------------------------------------------------------------------
Ran 0 tests in 0.000s
 
OK

Don't be fooled by the "OK" in the above message, it only shows up "OK" when nothing is actually running! I think none of the tests run should actually show an error, but personal feelings aside, this is a behavior you should pay attention to, especially when checking test results through a program run (for example, a continuous integration tool like TracisCI).

exception

Let's go back to the actual contents of test_primes.py and recall the output after running the python test_primes.py instruction:
 


$ python test_primes.py
E
======================================================================
ERROR: test_is_five_prime (__main__.PrimesTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_primes.py", line 8, in test_is_five_prime
  self.assertTrue(is_prime(5))
File "/home/jknupp/code/github_code/blug_private/primes.py", line 4, in is_prime
  if number % element == 0:
ZeroDivisionError: integer division or modulo by zero
 
----------------------------------------------------------------------
Ran 1 test in 0.000s

These outputs tell us that the result of one of our tests failed not because an assertion failed, but because an uncaught exception occurred. In fact, the unittest framework returned without being able to run our test due to an exception thrown.

The problem here is clear: the scope of the modular operation we used included 0, so we performed an operation of dividing by 0. To solve this problem, we can simply change the starting value from 0 to 2 and say that modulating 0 is wrong, but modulating 1 is always true (and a prime number is only divisible by itself and 1, so we don't have to check 1).

solves problems

A failed test caused us to change the code. Once we correct this error (change the line in s_prime to for in range(2, number):), we get the following output:
 


$ python test_primes.py
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

Now that the error has been fixed, does this mean we should delete the test_is_five_prime test method (because obviously it will not always pass the test)? It shouldn't be deleted. Because passing tests is the ultimate goal unit tests should be deleted as little as possible. We have tested that the syntax for is_prime is valid, and, in at least one case, it returns the correct result. Our goal is to create a set of tests that all pass (logical grouping of unit tests), although some may fail at first.

test_is_five_prime is used to handle a "non-special" prime number. Let's make sure that it also handles non-prime Numbers correctly. Add the following method to the PrimesTestCase class:
 


print(my_string[len(my_string)])
0

Notice that we now add an optional msg parameter to the assert call. If the test fails, our information is printed to the console and provides additional information to the person running the test.

boundary case

We have successfully tested two common cases. Now let's consider use cases with boundary cases, or with unusual or unexpected inputs. When testing a function whose range is a positive integer, the examples in the boundary case include 0, 1, negative Numbers, and a large number. Now let's test some of them.

Adding a test for 0 is easy. We expect ? is_prime(0) returns false, because, by definition, primes must be greater than 1.
 


print(my_string[len(my_string)])
1

Alas, the output is:
 


print(my_string[len(my_string)])
2

0 is incorrectly judged to be prime. We forgot, we decided to skip zeros and ones in the range of Numbers. Let's add a special check on them.
 


def is_prime(number):
  """Return True if *number* is prime."""
  if number in (0, 1):
    return False
 
  for element in range(2, number):
    if number % element == 0:
      return False
 
  return True

Now the test has passed. How should our function handle a negative number? It is important to know the output before writing the test case. In this case, any negative number should return false.
 


print(my_string[len(my_string)])
4

Here we feel like we're checking all the Numbers from minus 1 to minus 9. It is perfectly legal to call the test method in a loop, or to call the assert method multiple times in a test. We can rewrite the code in a (more detailed) way below.
 


print(my_string[len(my_string)])
5

These two are completely equivalent. Except when we ran the loop version, we got something we didn't really want:


print(my_string[len(my_string)])
6

Well... we know the test failed, but on what negative number did it fail? Quite usefully, Python's unit test framework does not print out the expected and actual values. We can move on to one of two ways to solve the problem: with the msg parameter, or by using a third-party unit testing framework.

Using the msg parameter to assertFalse only makes us realize that we can format strings to solve the problem.
 


print(my_string[len(my_string)])
7

Thus, the following output information is given:
 


python test_primes
...F
======================================================================
FAIL: test_negative_number (test_primes.PrimesTestCase)
Is a negative number correctly determined not to be prime?
----------------------------------------------------------------------
Traceback (most recent call last):
File "./test_primes.py", line 22, in test_negative_number
  self.assertFalse(is_prime(index), msg='{} should not be determined to be prime'.format(index))
AssertionError: True is not false : -1 should not be determined to be prime
 
----------------------------------------------------------------------
Ran 4 tests in 0.000s
 
FAILED (failures=1)

properly fixes code

As we can see, the negative of failure is the first number: -1. To solve this problem, we can add a special check for negative Numbers, but unit tests are not written to blindly add code to detect boundary conditions. When a test fails, we should step back and determine the best way to solve the problem. In this case, we should not add an extra if:
 


print(my_string[len(my_string)])
9

The following code should be used first:
 


def is_prime(number):
  """Return True if *number* is prime."""
  if number <= 1:
    return False
 
  for element in range(2, number):
    if number % element == 0:
      return False
 
  return True

In the latter code, we find that if the parameter is less than or equal to 1, the two if statements can be merged into a statement that returns false. This is not only more concise, but also fits nicely with the definition of a prime number (a number that is larger than 1 and divisible only by 1 and itself).

third-party testing framework

We could have also solved the problem of failing the test by using a third-party testing framework with too little information. The two most commonly used are py.test and nose. The following result is obtained by running statement py.test-l (-l is the value of the local variable shown).
 


#! bash
 
py.test -l test_primes.py
============================= test session starts ==============================
platform linux2 -- Python 2.7.6 -- pytest-2.4.2
collected 4 items
 
test_primes.py ...F
 
=================================== FAILURES ===================================
_____________________ PrimesTestCase.test_negative_number ______________________
 
self = <test_primes.PrimesTestCase testMethod=test_negative_number>
 
  def test_negative_number(self):
    """Is a negative number correctly determined not to be prime?"""
    for index in range(-1, -10, -1):
>      self.assertFalse(is_prime(index))
E      AssertionError: True is not false
 
index   = -1
self    = <test_primes.PrimesTestCase testMethod=test_negative_number>
 
test_primes.py:22: AssertionError

As you can see, some of the more useful information. These frameworks provide more functionality than mere detailed output, but the problem is that they only know that they can exist and extend the functionality of the built-in unittest test package.

conclusion

In this article, you learned what unit tests are, why they are so important, and how to write tests. That said, it's important to note that we've only scratched the surface of the test methodology, and that more advanced topics such as test case organization, continuous integration, and test case management are good topics for readers who want to learn more about testing in Python.

      reorganizes/cleans up code       code without exposing its internal data or functions and without using other code's internal data or functions