Testing is often an after though of development. It is a check when we finish that everything is correct. We build something we want to pass a test and then build the test. This make writing the code harder as we don't have a clear idea of what the code should do.
Instead we can write the test as part of defining the code. If we test we define both the what and the how of code. We decide in the test what the input should be to a function and what the output we want from it is. We can define limits of the code as well by testing multiple inputs, and purposely raising errors and incorrect results.
Once we have code we can tell straight away if it works correctly by running the tests. For any errors found we have better localisation based on the failed tests. This can make debugging faster and more focused. Extra tests can always be added later for errors not caught by the original testing. The method of writing tests before code is test driven development.
In Python there are several different packages that can be used to implement testing. Here we will focus on unittest. We will present examples of different standard tests. For this we will use a few simple functions and a develop tests on their returns. We will also provide a breif introduction to debugging and testing tools in PyCharm.
We have three functions with particular characteristics.
Our first function is simple with a deterministic output which is not a list. It just returns the text "Hello World!".
Our second function has a non-deterministic list output. We can use this to demonstrate assert statements which don't need an exact output. It produces a list of 10 random numbers. It starts with a list between 1 and 2 but then multiplies them by the mult value and then subtracts the sub value. This modifies the range of the resultant numbers. We can check if it is in the correct range using our tests.
Our final function returns a deterministic list of 12 values. The angles of numbers on a clock in radians. This allows us to use some of our list checks for items being included and checks for approximate values for when we don't want to calculate or write out long floats.
We can test for a return from a function by assert that the return value is not None in python. For this test we will use our "Hello World" function.
which work
Here we declare a test class, TestScratch, to use unittest. Normally we would do this in a separate file and import the module or class being tested. We then start a test_hello_world function in which we will add all the assert statements to test this function. Our first test is that it returns anything. This is done with assertIsNotNone and we make sure to attach a message. This way if the test is failed we know what has gone wrong.
We will look at the results of running tests later in the Tests In PyCharm section.
We can test more acurately when we know what a function should return. We do this by check that the return equals the correct value. We can do multiple of these with different inputs for functions with an input but in this case we will just check the return string of our first function. We add this additional test to the existing test_hello_world.
We use the assertEqual() function to compare the string we are expecting with the actual return. We also make sure to add a message again indicating that we have tested the string and it is incorrect. That way when we run these tests if it fails we know if it didn't return anything or if it returned the wrong value.
Next we check the size of a return. Many different returns in Python have a size and shape and sometimes we don't know what exactly the contense should be but the correct shape of the return is known. In the case of our functions we don't know exactly what will be in the return from the random_number() function but we do know that it should have a length of 10.
We use the assertEqual() function in this case. By directly calling and checking the length we can easily adapt to other structures. Instead of calling len() we can call shape() or any other relevant metadata for your shape.
We check for type because sometimes when we write a function we can forget where we are going to use it and return something in the wrong format. We make sure that our returns are the type we think they should be with a type check. We'll demonstrate this on our first and second functions.
We use the assertIsInstance() function for type checking. You'll notice that in the first function we don't add this check to the end of the function. There is no point in checking its type if we know it is an exact match to the string. We check type before equality. In the second usage we check the type of the values inside the list. This currently only checks the first value, depending on the function you may need to check all values are of the correct type.
We some times want to check a statement is True or False. To do this we need to be able to write an assert based on a logic function. We use this to check if returns from random_numbers() are in the correct range.
We test this twice. First with the existing return which uses 1 as the multiplier and 0 for subtraction which should have no effect on the range of the output numbers, 1 to 2. The second time we test with a multiplier of 7 and a subtraction of 4 to make sure these are being correctly applied to give numbers in the correct range, now 3 to 10.
The boolean tests do not however test cases with negative or float values. If we were following Test Driven Development, we would want to include those cases in our test to help decide what should happen with those inputs. This could mean testing for an error if a float was given since the function should not work with float. Say this is our goal we will add that test to our function.
We test for errors we want to happen by running the function with the assertRaises() function. This will catch the error and confirm it is of the correct type. If not it will still allow us to provide a meaningful message about what is incorrect.
We often want to check that a list contains a particular expected element. For this test we look to our third function which returns a deterministic list. We can check that this list starts from 0 and that it has the correct 4th angle which on a clock should be 90°.
We test if each of these angles is in our list separately with their own messages so we will know which is missing. We also add the inverse test to check that an angle in degrees rather than radians isn't present. If we don't care about the position of an item in a list this is a great way of checking it is present and correctly typed.
Some times near enough will do. If I want to for instance check a value is of the right magnitude or in the correct region I could use the above boolean method or I could check that it is approximately equal to another number given an allowable variation. For instance if I want to check the angles given with out using math.pi I would need to write out very long numbers or...
Here we test the 7th number in our array which should be halfway around our circle and in radians that should be π. We compare the value to π writen to 3 decimal places with a 2 decimal place accuracy. We are clear about the accuracy we looked for in our error message so that we know exactly what we have tested.
Now that we have written our tests we want to run them to see what the result is. In PyCharm we have several options for running tests. From with in the test file PyCharm presents us with several green arrows.
Each of these arrows can be used to run the tests. We can run the tests for each function separately or the entire test class at once.
When clicked we can choose to run the tests or to debug the tests. We will start by running the entire class of Tests to see if they all pass.
The above shows the results of the three tests we have run. We can see at the two that 2 tests have passed but 1 has failed. To the side we can see that the failed test is test_random_numbers. Then we can see from the Failure message that the message returned is "Faild to raise type error for float input". To check this function on its own we could rerun just this test. Sometimes there can be some interference between tests so this is often worth checking. In this case though we can see from the code for this function that it never raises a type error so we should add that to the function. Then we will run the test on its own to see if the correction has worked.
We can see from the top bar that here we only ran the test for random_numbers and then we can see that the test ran fine with no failures. If we had a harder to find issue we could put breakpoints into our function and test code and run in debug mode instead allowing us to inspect the code at those breakpoints. For instance to check if the type is correct at the start of the function we would put a break point on the first line and see what the inspector says when we debug the test.
We place a break point by clicking on the space to the left of the code and leaving a red dot. This line is then highlighted when the debuger stops here. We then look at the debug interface at the bottom of the window.
This tells us that at this break point mult and sub are both ints and have the values 1 and 0. This is because our test calls this function twice. It is the third time that we have a non-int value. We can tell the debugger to continue by clicking the green pause/play button on the left (For functions that take a while to run you can also use this to pause the system and see what is happening.) We will click this twice to see our float input.
Here we can see that the type for mult is now listed as float. This debugging interface is a great way to find problems in your code and well written tests can help guide you to where to put break points to isolate errors quickly. In particular when we find the sources of problems in this way it is good practise to write a test that checks for that particular problem.
For larger pieces of work with multiple class and module files, and therefor multiple testing files, PyCharm will also run all your packages tests if you right click on the test folder and select: Run "Python tests in test..."