The correct way to develop any piece of code is to use automatic testing. The advantages are:
We suggest developing tests in parallel to the code. Good design of tests is an art of its own and there is rarely an investment which guarantees such a good pay-off in development time savings as the investment in good tests.
Now we will go through the implementation of a simple algorithm with the automated testing methods in mind.
Let us examine automated testing for the bisection algorithm. With this algorithm, a zero of a real valued function is found. It is described section Exercise 4 in Chapter 7, Functions. An implementation of the algorithm can have the following form:
def bisect(f, a, b, tol=1.e-8): """ Implementation of the bisection algorithm f real valued function a,b interval boundaries (float) with the property f(a) * f(b) <= 0 tol tolerance (float) """ if f(a) * f(b)> 0: raise ValueError("Incorrect initial interval [a, b]") for i in range(100): c = (a + b) / 2. if f(a) * f(c) <= 0: b = c else: a = c if abs(a - b) < tol: return (a + b) / 2 raise Exception( 'No root found within the given tolerance {}'.format(tol))
We assume this to be stored in the bisection.py
file. As the first test case, we test that the zero of the function f(x) = x is found:
def test_identity(): result = bisect(lambda x: x, -1., 1.) expected = 0. assert allclose(result, expected),'expected zero not found' test_identity()
In this code, you meet the Python keyword assert
for the first time. It raises AssertionError
exception if its first argument returns the False
value. Its optional second argument is a string with additional information. We use the function allclose
in order to test for equality of floats.
Let us comment on some of the features of the test function. We use an assertion to make sure that an exception will be raised if the code does not behave as expected. We have to manually run the test in the test_identity()
line.
There are many tools to automate this kind of call.
Let us now set up a test that checks if bisect
raises an exception when the function has the same sign on both ends of the interval. For now, we will suppose that the exception raised is a ValueError
exception. In the following example, we will check the initial interval [a,b]. For the bisection algorithm it should fulfill a sign condition:
def test_badinput(): try: bisect(lambda x: x,0.5,1) except ValueError: pass else: raise AssertionError() test_badinput()
In this case, an AssertionError
is raised if the exception is not of the ValueError
type . There are tools to simplify the preceding construction to check that an exception is raised.
Another useful test is the edge case test. Here we test arguments or user input, which is likely to create mathematically undefined situations or states of the program not foreseen by the programmer. For instance, what happens if both bounds are equal? What happens if a > b?
def test_equal_boundaries(): result = bisect(lambda x: x, 0., 0.) expected = 0. assert allclose(result, expected), 'test equal interval bounds failed' def test_reverse_boundaries(): result = bisect(lambda x: x, 1., -1.) expected = 0. assert allclose(result, expected), 'test reverse interval bounds failed' test_equal_boundaries() test_reverse_boundaries()
3.137.223.190