Start by reading the README
for the Calculator project on GitHub, and then install the application.
All done? Great! Let’s see what this project is all about.
You currently work at a company that needs to set up a super-calculator. Because we’ve only recently started the project, we only have three classes that operate the calculator. For the time being, the calculator can only handle four basic operations (addition, subtraction, multiplication, and division).
The main.py
file is the entry point for the application. The controller.py
is the file that contains the application logic. The code for the user to interact with the program is contained in view.py
. Then, the code will use the functions held within operators.py
to perform the calculations.
Here’s a screencast to introduce you to the project that you’re going to use to create your very first unit tests. Watch the video several times if you need to!
Wait, Why Do We Create Tests?
Setting up tests for a project like this might seem futile. After all, we can quickly check if your addition actually performs an add operation. But what about a much bigger project? There might be hundreds of classes like these, and checking that each one does what we want it to do would be a long and laborious process.
Worse still, imagine if you had a feature that doesn’t return an error, but that also doesn’t give us the result we expect. This kind of bug is likely to slip through the net. But if my code is properly covered by testing, errors can be spotted and quickly corrected.
“Ah, that’s the way it goes...” you might be thinking. Well, yes, but it would be a shame to be so pessimistic when an error like this could easily be avoided. And for an organization as large as Facebook, think of the repercussions!
Identify What Needs Testing
The first question to ask when adding tests to a project is: what do we want to test?
Our program’s outcome? In our case, we can test that the result should always be displayed. Or do we want to test the code’s integrity, function by function?
Define an Initial Strategy
Testing only the final result of our program isn’t a great idea because it’s too broad an approach. Let’s take the opposite perspective and create unit tests that will check that each method in our code produces the results we want. This will reassure us that every part of our program is rock solid!
Our code contains 32 classes, functions, and variables that we could test. The initial strategy could therefore involve creating as many tests as there are functions.
But this is far from ideal. Let’s just think about this for a moment. Why do we want to create tests? So that we can add a feature later on and be certain that this feature won’t have an adverse effect on the previous features.
So, there’s no point in testing the full program logic in detail. Why? Because if I need to amend my code to improve it, this is going to fail a number of tests even though my program still works. This is because I will have written a number of tests that are dependent on how my program was written.
Reminder About Objects
To better understand what we should be testing, let’s go back to some of the concepts around object-oriented programming.
Every object can be viewed as a space shuttle in orbit. The space shuttle has no idea what’s going on in the other space shuttles. It only knows what it needs to know to carry out its own function.
Space shuttles communicate with each other via incoming and outgoing messages. A shuttle can also send internal messages to staff within the shuttle.
Test an Interface
It is good practice to test only the incoming messages, i.e., the public methods, and not the private methods. We consider an object to be a black box that contains everything it needs to function correctly. From the outside, we don’t need to know how it sends us the information. We just check that the information matches what we’re expecting to receive.
This means we’re going to test the public interface of an object and not its internal workings. We then have greater flexibility, not just in terms of our testing, but also in terms of our object configuration. You can easily change an object’s code without needing to update your tests. So, for example, if one day you change the way you calculate the data but the returned result still looks how you expected, the test will still be valid and you won’t need to rewrite it.
Prepare a Test Plan
As you can tell, it’s not very easy to identify all of the scenarios that you need to test. This is why it’s important to prepare a test plan. It enables you to identify the items and features that require testing. It also allows you to organize and plan the test execution. A well-defined test plan can save you considerable amounts of time.
The test plan can contain varying amounts of detail, but there are some important points to specify for each test case:
The type of test (unit, integration, functional, etc.)
The unit of code being tested
Test inputs
Expected results
Time For You to Practice
Let’s take a real example and try to identify the different test scenarios. Below is a class that performs operations on two values (addition, subtraction, multiplication, and division).
import re
class Calculator:
def __init__(self):
self.left_value = 0
self.right_value = 0
def calculate(self, operation):
if self._check_and_set_value(operation):
if "+" in operation:
return self.left_value + self.right_value
elif "-" in operation:
return self.left_value - self.right_value
elif "*" in operation:
return self.left_value * self.right_value
elif "/" in operation:
try:
return self.left_value / self.right_value
except ZeroDivisionError:
return "Invalid operation : Zero Division Error"
else:
return "Invalid operation"
else:
return "Invalid operation"
def _check_and_set_value(self, operation):
operation = operation.replace(" ", "")
values = re.split('\+|\-|\*|\/', operation)
if len(values) == 2:
try:
self.left_value = float(values[0])
self.right_value = float(values[1])
return True
except ValueError:
return False
else:
return False
First of all, we need to identify the public methods for our Calculator
class. We want to test the incoming messages and then check the outgoing messages from our black box. So, we’re going to test all of the public methods for the Calculator
class.
However, it’s important to point out that we’re not going to create one test for each public method, but as many tests as there are possible results returned by the public method. It’s quite normal to have several tests for just one public method.
In this screencast, we’re going to identify the different scenarios for the Calculator
class together, adopting the best practices that we recommend on this course.
Over to You!
The company needs a test plan to prepare a set of tests that the teams need to set up over the next few days.
So, your mission is to create a detailed test plan for each file in the super-calculator project (solely for unit testing).
Let’s Recap!
You only need to test an object’s public interface and not its internal functions.
Create a test plan to help organize and plan the test execution.
Create as many tests as there are possible results returned from the public method.
So, are you ready for your first test? Follow me to the next chapter!