One of the most common puzzles for novice programmers is the subject of testing. They vaguely think "unit testing" is good, and they should also do unit tests. But they do not understand the true meaning of the word. If that sounds like talking about you, don't be afraid! In this article, I'll cover what unit tests are, why it works, and how to unit-Test Python code.
What is a test?
Before discussing why testing is useful and how to test it, let's take a few minutes to define what "unit tests" are. In general programming terminology, "testing" refers to helping you determine whether there is an error in your program by writing code that can be called, independent of the code of your actual application. This does not prove that your code is correct (it is only possible in very limited cases). It simply reports whether the situation that the tester thinks is being handled correctly.
Note: When I use "test" one time, I refer to "automated testing", that is, these tests are run on the machine. A "manual test" is a separate concept when a person runs a program and interacts with it to discover a vulnerability.
What kind of situation can the test check out? Grammatical errors are accidental misuse of language, such as
My_list. Append (foo)
One of the extra "." Behind. A logic error is raised when the algorithm (which can be seen as a "way to solve the problem") is incorrect. The programmer may forget that Python is "0 indexed" and tries 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 checked out. For example, when a user enters a number greater than 100, or suspends the site when the site search is unavailable, the program crashes.
All of these errors can be checked by careful testing of the code. Unit testing, specifically a test in a delimited unit of code. A unit can be an entire module, a single class or function, or any code between the two. However, it is important that the test code be isolated from the other code that we did not test (because the other code itself has errors that would 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 that Is_prime is correct, because this function is called in Print_next_prime. In this case, the Print_next_prime function is a unit, and the Is_prime function is another unit. Since unit tests only test one unit at a time, we need to think carefully about how we can test print_next_prime accurately. (More about how to implement these tests later).
So what should the test code look like? If the previous example has a file called primes.py, we can write the test code in a file called test_primes.py. The following is the most basic content in test_primes.py, such as the following test sample:
Import Unittestfrom primes Import Is_prime class Primestestcase (unittest. TestCase): "" " Tests for ' primes.py '. " "" " def test_is_five_prime (self): "" "was five successfully determined to be prime? " " Self.asserttrue (Is_prime (5)) If __name__ = = ' __main__ ': unittest.main ()
This file passes a test case:? Test_is_five_prime. A unit test was created. A test framework embedded in Python unittest. When Unittest.main () is called, any member function named after the start of test will be run, and they are unittest. A derived class of testcase and is an assertion check. If we run the test by entering Python test_primes.py, we can see the output of the UnitTest framework on the console:
$ python test_primes.pye======================================================================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----------------------------------------------------------------------the Ran 1 test in 0.000s
The individual "E" represents the result of our unit test (if it succeeds, it prints out a ".") )。 We can see that our test failed, and the line of code that caused the failure, and any exception information that was thrown.
Why do you want to test?
Before we go on that example, ask a very important question: "Why is the test valuable to me"? This is a fair question, and one that is often asked by people who are unfamiliar with code testing. After all, testing takes some time, and we can use that time to compile code, why test instead of doing the most productive things?
There are many answers that can be answered effectively, and I have listed the following points:
Testing can ensure that your code works under a given set of conditions
The test ensures correctness under a range of conditions. Grammatical errors are almost certainly detected by testing, and the basic logic of a unit of code can be tested to ensure correctness under certain conditions. Again, it is not to prove that the code is correct under any conditions. We are simply aiming for a more complete set of possible conditions (for example, you can write a test to monitor when you call My_addition_function (3, ' refrigerator), but you do not have to detect all possible strings for each parameter)
Testing allows people to ensure that changes to the code do not break existing functionality
This is especially useful when refactoring code. Without testing in place, you won't be able to make sure that your code changes without destroying something that worked properly before. If you want to change or rewrite your code and want to not break anything, proper unit testing is necessary.
Testing forces people to think about code in unusual conditions, which may reveal a logical error
Writing tests forces you to think about problems that your code might encounter under abnormal conditions. In the example above, the My_addition_function function can add two numbers. A simple test that tests basic correctness will call My_addition_function (2,2) and assert that the result is 4. However, further testing may be done by calling My_addition_function (2.0,2.0) to test whether the function is capable of correctly operating floating-point numbers. The defensive coding principle indicates that your code should be able to fail normally in the case of illegal input, so when testing, you should throw an exception when the string type is passed as a parameter to the function.
Good testing requires modularity, decoupling code, which is a sign of good system design
The overall approach to unit testing is to make it easier by loosely coupling the code. If your application code calls the database directly, for example, the logic that tests your application relies on a valid database connection, and the test data exists in the database. On the other hand, code that isolates external resources is more likely to be replaced by mock objects during testing. Because of the need, the test-capable applications (people) have been designed with modularity and loose coupling.
Anatomy of a unit test
By continuing with the previous examples, we will 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)
At the same time, the file test_primes.py contains the following code:
Import Unittestfrom primes Import Is_prime class Primestestcase (unittest. TestCase): "" " Tests for ' primes.py '. " "" " def test_is_five_prime (self): "" "was five successfully determined to be prime? " " Self.asserttrue (Is_prime (5)) If __name__ = = ' __main__ ': unittest.main ()
Make assertions
UnitTest is part of the Python standard library and is a good starting point for us to start the Unit Test tour. One unit test includes one or more assertions (some statements that declare some of the properties of the code being tested are true). When you go to school, the word "assert" literally means "stating the facts". In unit tests, assertions are equally useful.
Self.asserttrue is more like a self-explanatory. It can declare that passing past parameters evaluates to True. UnitTest. The TestCase class contains many assertion methods, so be sure to check the list and choose the appropriate method for testing. If you use asserttrue in every test, you should consider an anti-pattern because it increases the cognitive burden of the reader in the test. The correct way to use assertions should be to enable the test to clearly state what is being asserted (for example, obviously, just by glancing at the name of the Assertisinstance method, you know it is the parameter).
Each test should test for a separate, specific code, and should be given the relevant naming. Research on the unit Test discovery mechanism (mainly in the python2.7+ and 3.2+ versions) shows that the test method should be named after the Test_ prefix. (This is configurable, but its purpose is to identify test methods and non-test practical methods). If we change the name of Test_is_five_prime to Is_five_prime, the following message will be printed when we run the test_primes.py in Python:
$ python test_primes.py----------------------------------------------------------------------Ran 0 tests in 0.000s OK
Do not be fooled by the "OK" in the above information, only when the test is not really running the time will show "OK"! I think a test is not running should actually show an error, but personal feeling aside, this is a behavior you should pay attention to, especially when the program run to check the results of the test (for example, a continuous integration tool, such as Tracisci).
Abnormal
Let's go back to the actual content of test_primes.py and recall the output after running the python test_primes.py command:
$ python test_primes.pye======================================================================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----------------------------------------------------------------------the 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, because an exception was thrown, the unittest framework was not able to run our tests and returned.
The problem here is clear: we use the calculation range of the modulo operation to include 0, thus performing a operation divided by 0. To solve this problem, we can easily change the starting value from 0 to 2, and point out that modulo 0 is wrong, and the 1 modulo is always true (and a prime number can only be divisible by itself and 1, so we don't need to check 1).
Solve the problem
A failed test allowed us to modify the code. Once we have changed this error (change the line in S_prime to for element in range (2, number):), we have the following output:
$ python test_primes.py.----------------------------------------------------------------------Ran 1 Test in 0.000s
Now that the error has changed, does this mean that we should delete the Test_is_five_prime test method (because it is obviously not going to pass the test all the time)? should not be deleted. Unit tests should be removed as little as possible because passing tests are the ultimate goal. We have tested that the syntax of Is_prime is valid and, at least in one case, it returns the correct result. Our goal is to create a set of tests that can be fully passed (logical grouping of unit tests), although some may fail at first.
Test_is_five_prime is used to process a "non-special" prime number. Let's make sure it also handles non-prime numbers correctly. Add the following methods to the Primestestcase class:
def test_is_four_non_prime (self): "", "" is a four correctly determined isn't to be prime? "" Self.assertfalse (Is_prime (4), msg= ' Four are not prime! ')
Note that we have added an optional msg parameter to the assert call. If the test fails, our information will be printed to the console and provide additional information to the person running the test.
Boundary conditions
We have successfully tested two common cases. Now let's consider the use cases of the boundary case, or those unusual or unexpected inputs. When testing a function whose scope is a positive integer, the instance of the boundary case consists of 0, 1, negative numbers, and a large number. Now let's test some of them.
Adding a test to 0 is simple. What do we expect? Is_prime (0) returns false because, by definition, the prime number must be greater than 1.
def test_is_zero_not_prime (self): "" "was zero correctly determined not to be prime? " " Self.assertfalse (is_prime (0))
Unfortunately, the output is:
Python test_primes.py. F======================================================================fail:test_is_zero_not_prime (__main__. Primestestcase) is zero-correctly determined not-to-be prime?--------------------------------------------------------- -------------Traceback (most recent call last): File "test_primes.py", line +, in Test_is_zero_not_prime Self.assertfalse (is_prime (0)) Assertionerror:true is not false---------------------------------------------------- ------------------Ran 3 Tests in 0.000s FAILED (Failures=1)
0 are wrongly judged as prime numbers. We forgot that we decided to skip 0 and 1 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< C7/>return True
Now the test passed. What should our function do with a negative number? It is important to know the output before writing this test case. In this case, any negative numbers should return false.
def test_negative_number (self): "" "is a negative number correctly determined " "" For index in range ( -1, -10,-1): self.assertfalse (Is_prime (index))
Here we feel to check all numbers from-1 to-9. It is very legal to call the test method in a loop, and it is also possible to invoke the assertion method multiple times in a test. We can rewrite the code in the following way (in more detail).
def test_negative_number (self): "" "is a negative number correctly determined " "" Self.assertfalse (Is_prime ( -1)) Self.assertfalse (Is_prime ( -2)) Self.assertfalse (Is_prime ( -3) ) Self.assertfalse (Is_prime ( -4)) Self.assertfalse (Is_prime ( -5)) Self.assertfalse (Is_prime ( -6) ) Self.assertfalse (Is_prime ( -7)) Self.assertfalse (Is_prime ( -8)) Self.assertfalse (Is_prime (-9))
These two are completely equivalent. In addition to when we run the loop version, we get a message we don't really want:
Python test_primes.py ... F======================================================================fail:test_negative_number (__main__. Primestestcase) is a negative number correctly determined don't to be prime?---------------------------------------------- ------------------------Traceback (most recent call last): File "test_primes.py", line A, in Test_negative_number Self.assertfalse (Is_prime (index)) Assertionerror:true is not false------------------------------------------------ ----------------------Ran 4 tests in 0.000s FAILED (Failures=1)
Yes ... We know that the test failed, but on which negative number did it fail? Very useless, the Python unit test framework does not print out expected values and actual values. We can step into two ways and use one of them to solve the problem: through the MSG parameter, or by using a third-party unit testing framework.
Using the MSG parameter to Assertfalse only allows us to realize that we can use string formatting to solve the problem.
def test_negative_number (self): "" "is a negative number correctly determined " "" For index in range ( -1, -10,-1): self.assertfalse (Is_prime (index), msg= ' {} should not being determined to being prime '. Forma T (index))
The following output information is given:
Python test_primes ... F======================================================================fail:test_negative_number (Test_primes. Primestestcase) is a negative number correctly determined don't to be prime?---------------------------------------------- ------------------------Traceback (recent): File "./test_primes.py", line A, in Test_negative_number Self.assertfalse (is_prime (index), msg= ' {} should not being determined to being prime '. Format (index)) assertionerror:true Is isn't false:-1 should not being determined to being prime------------------------------------------------------------------- ---Ran 4 tests in 0.000s FAILED (Failures=1)
Properly fix the code
We see that the negative number of the failure is the first digit:-1. To solve this problem, we can add a special check for negative numbers, but the purpose of writing unit tests is not 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 additional if:
def is_prime (number): "" " Return True if *number* is prime. " " If number < 0: return false if number in (0, 1): return False for element in range (2, number): if n Umber% Element = = 0: return False return True
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 fals E return True
In the latter code, we find that if the argument is less than or equal to 1 o'clock, two if statements can be combined into a statement with a return value of false. This is not only more concise, but also nicely fit the definition of prime (a number larger than 1 and divisible by 1 and itself).
Third-party testing framework
We could have solved this problem by using a third-party testing framework that caused the test to fail because of too little information. The two most commonly used are py.test and nose. The following results can be obtained by running the statement py.test-l (-l to display the value of the local variable).
#! Bash Py.test-l test_primes.py============================= test session starts ============================== Platform Linux2--Python 2.7.6--pytest-2.4.2collected 4 items test_primes.py ... F =================================== Failures ===================================_____________________ Primestestcase.test_negative_number ______________________ self =
def test_negative_number (self): "" "is a negative number correctly determined isn't to be prime? " " For index in range ( -1, -10,-1):> self.assertfalse (is_prime (index)) E assertionerror:true are not false index< c7/>= -1self =
test_primes.py:22:assertionerror
As you can see, some of the more useful information. These frameworks provide more functionality than simply more verbose output, but the problem is simply knowing 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, to be aware that we just cut open the surface of the test methodology, more advanced topics such as the organization of test cases, continuous integration, and the management of test cases are good topics for readers who want to learn more about testing in Python.
- Reorganization/cleanup of code without changing its functionality
- Internal data or functions that do not expose their internal data or functions and do not use other code when compiling code