As a web development manager, it’s part of my job to be involved in defining best practices for my team. This means defining a set of standard practices that will benefit the whole team throughout the entire development process. A good example of one such best practice would be code testing.
Testing, for web developers, is usually a fairly disorganised affair. Most of the time, we test “on the job”, i.e., we code something and then we run it; if it breaks, we fix it. In fact, often, the thought of actually structuring our testing process can seem somewhat of an over complication. This is certainly not the case.
The benefits of a structured testing process are legion, as any software engineer will attest. At the very least, it provides confidence in our code – something which can only be of incredible benefit when code is shared across a wider team. At its best, the tests we write can influence the way we develop the code being tested; and save effort, time, and money.
In this post I’m going to look at “unit testing”, and how it can be of benefit to web developers in general.
What is unit testing?
Unit testing is the process of validating and verifying the individual “units” of code in a system. A unit is the smallest testable part of a system or application. In the majority of cases within web development, these units are usually individual functions or methods of our object-oriented back-end and front-end code.
A note on language
I’ll use Python as the language for all examples in this post as it’s my current weapon of choice (and the interactive interpreter is very handy for trying out ideas); if you’ve never seen Python syntax, don’t worry, it’s easily understandable (if you want more information, take a look at the Python tutorial).
Writing a simple test
To begin with, let’s imagine we have a function for calculating the area of a rectangle:
def get_area_of_rect( width, height ): return width * height
Let’s paste this function into the interactive interpreter, so that we can play with it:
>>> def get_area_of_rect( width, height ): ... return width * height ... >>>
Now if we call it, we’ll receive the area of our rectangle like so:
>>> get_area_of_rect( 2, 3 ) 6 >>>
Now if we wanted to test this function, we could call it with a some defined parameters and test the output against our expectations:
def test_get_area_of_rect(): width = 2 height = 3 expected_result = 6 result = get_area_of_rect( width, height ) if result == expected_result: print "Test passed: Received expected result!"
Now if we run our test, we should see the following:
>>> test_get_area_of_rect() Test passed: Received expected result! >>>
Brilliant; our code passed the test because the expected result was the same as the actual result – however, that’s actually not very useful to us. It’s far more important that our tests can trap failing code than code that works.
get_area_of_rect function had been updated as follows:
def get_area_of_rect( width, height ): return width / height
Anyone with rudimentary maths skills can tell that this will no longer return the correct area of a rectangle. In fact, if we rerun our test, we’ll get no output:
>>> test_get_area_of_rect() >>>
A fat lot of good that is. If our code is failing for some reason, we want to know about it. After all, that’s the whole reason for testing it.
Let’s update the
if statement in our
if not result == expected_result: raise AssertionError else: print "Test passed: Received expected result!"
In fact, we could probably dump the
else clause since it’s of little real importance except for logging purposes:
if not result == expected_result: raise AssertionError
That’s nicer, but Python provides us a shortcut to this
if not … raise AssertionError syntax with the
assert result == expected_result
With that in mind, the
test_get_area_of_rect function should now look like this:
def test_get_area_of_rect(): width = 2 height = 3 expected_result = 6 result = get_area_of_rect( width, height ) assert result == expected_result
If we run our test again, we should receive more constructive output, considering our
get_area_of_rect function is still dividing:
>>> test_get_area_of_rect() Traceback (most recent call last): File "", line 1, in File "", line 8, in test_get_area_of_rect AssertionError >>>
We now have a decent automated test, but we’re only checking a single expected result. If, for instance, we passed in a width of 1 and a height of 1, both multiplication and division would return the same result: 1. This means that we should pass multiple sets of width and height parameters and test against their expected results. Since these parameters will be constant, let’s define them as tuples and loop through them, testing each in turn:
def test_get_area_of_rect(): known_values = ( ( 0, 0, 0 ), ( 1, 1, 1 ), ( 2, 2, 4 ), ( 2, 3, 6 ), ( 3, 0, 0 ) ) for width, height, expected_result in known_values: result = get_area_of_rect( width, height ) assert result == expected_result
As you can see, we’re now passing several sets of parameters. Each set of parameters is valid, and as such, we should expect a successful outcome from our
Now if we run our test, we should get something like the following:
>>> test_get_area_of_rect() Traceback (most recent call last): File "", line 1, in File "", line 8, in test_get_area_of_rect File "", line 2, in get_area_of_rect ZeroDivisionError: integer division or modulo by zero >>>
Ah ha! By passing multiple sets of parameters we’ve uncovered more significant information regarding the
AssertionError that was raised earlier; we have a division by zero error on line 2 of
get_area_of_rect. This has supplied us with all the information we need to fix our error, so let’s take a look at that function again:
def get_area_of_rect( width, height ): # Line 1 return width / height # Line 2
Change the division back to multiplication, like so:
def get_area_of_rect( width, height ): # Line 1 return width * height # Line 2
And then rerun our test:
>>> test_get_area_of_rect() >>>
We get no output at all because the test passed successfully. As the old proverb goes, “no news is good news”.
Now if we add to our original code in the future, we’ll still be able to run this testing code to see what has been affected. What’s more, we can add to it by writing more tests, should the complexity grow. In fact, creating an extensive suite of tests is generally the aim of unit testing, but to do so, it’s a good idea to utilise a testing framework, which I’ll look at in the next post of this series.
Writing tests for code enables more efficient debugging, faster bug fixing, and an overall confidence in the quality of your code. Furthermore, the rest of your team (or whomsoever you’re sharing said code with) will equally have confidence thanks to the tests included.
In brief, here’s what we’ve learnt about unit testing:
- It's most important that your tests catch failure, not success.
- Test the smallest amount of code, or "unit", possible each time.
- Test as many different parameters, inputs, and outputs as possible.
- Run the tests often.
The next post
In the next post, I’ll be expanding on unit testing by looking at test frameworks, test architecture, code coverage, and other more advanced techniques that can improve your testing environment.