### Testing Part 3: The Triangle Example

In the last two posts, I talked about the philosophy of testing, and showed the basic framework that Python provides.

# The Triangle Example

This example is a classic from Glenford Myers’ *The Art of Software Testing*, first published in 1979. For all of the advances we’ve seen since then, it’s still a fantastic example of the things that can cause problems in software.

Given the integer lengths of the three sides of a triangle, determine whether the triangle is

scalene,isoceles, orequilateral.

For a basic refresher:

*Equilateral Triangle:*a triangle with 3 equal-length sides*Isoceles Triangle:*a triangle with 2 equal-length sides*Scalene Triangle:*a triangle with no equal-length sides

Here’s a first crack at implementing this function:

def classify_triangle(a, b, c): if a == b == c: return 'equilateral' elif (a == b) or (b == c): return 'isoceles' else: return 'scalene'

*You probably don’t want to use this code for anything, because it has errors in it.*

To start out with, lets test the equilateral case. First, we make a test that will fail:

import unittest # this pulls our function in from the other file: from triangle import classify_triangle class ClassifyTriangleTest(unittest.TestCase): def testEquilateral(self): self.assert_(False) if __name__ == '__main__': unittest.main()

$ python basic_triangle_test.py F ====================================================================== FAIL: testEquilateral (__main__.ClassifyTriangleTest) ---------------------------------------------------------------------- Traceback (most recent call last): File "basic_triangle_test.py", line 7, in testEquilateral self.assert_(False) AssertionError ---------------------------------------------------------------------- Ran 1 test in 0.000s FAILED (failures=1)

As expected, the test is executing, but failing. Now we can fill in the test, using checking against an incorrect result, to ensure that it’s working:

import unittest # this pulls our function in from the other file: from triangle import classify_triangle class ClassifyTriangleTest(unittest.TestCase): def testEquilateral(self): result = classify_triangle(2, 2, 2) self.assertEquals(result, None) if __name__ == '__main__': unittest.main()

Running it shows us that it’s failing (but working properly):

$ python basic_triangle_test.py F ====================================================================== FAIL: testEquilateral (__main__.ClassifyTriangleTest) ---------------------------------------------------------------------- Traceback (most recent call last): File "basic_triangle_test.py", line 8, in testEquilateral self.assertEquals(result, None) AssertionError: 'equilateral' != None ---------------------------------------------------------------------- Ran 1 test in 0.001s FAILED (failures=1)

You can see that the test is failing because the result is coming back as *‘equilateral’*, but we’re asserting that it’s equal to *None*. I do this while I’m writing my tests to ensure that I’m actually computing the value I’m expecting.

Changing the assert statement corrects the test and makes it run successfully:

import unittest # this pulls our function in from the other file: from triangle import classify_triangle class ClassifyTriangleTest(unittest.TestCase): def testEquilateral(self): result = classify_triangle(2, 2, 2) self.assertEquals(result, 'equilateral') if __name__ == '__main__': unittest.main()

$ python basic_triangle_test.py . ---------------------------------------------------------------------- Ran 1 test in 0.000s OK

Hopefully this next step is going to be pretty clear. Instead of writing about going through the same process for the isoceles and scalene cases, I’m going to jump ahead a smidge.

Here’s the completed set of basic tests:

import unittest # this pulls our function in from the other file: from triangle import classify_triangle class ClassifyTriangleTest(unittest.TestCase): def testEquilateral(self): result = classify_triangle(2, 2, 2) self.assertEquals(result, 'equilateral') def testIsoceles(self): result = classify_triangle(2, 2, 1) self.assertEquals(result, 'isoceles') def testScalene(self): result = classify_triangle(3, 4, 5) self.assertEquals(result, 'scalene') if __name__ == '__main__': unittest.main()

And here’s the command-line output:

$ python basic_triangle_test.py ... ---------------------------------------------------------------------- Ran 3 tests in 0.000s OK

# We’ve Tested Everything! We’re Done!

… not so fast.

There’s still a few problems. The first one I’m going to address is in testIsoceles(). This test is currently only checking one case: when a==b. Lets beef it up a bit, by addressing all three permutations:

def testIsoceles(self): result = classify_triangle(2, 2, 1) self.assertEquals(result, 'isoceles') result = classify_triangle(2, 1, 2) self.assertEquals(result, 'isoceles') result = classify_triangle(1, 2, 2) self.assertEquals(result, 'isoceles')

Running the test suite again:

$ python basic_triangle_test.py .F. ====================================================================== FAIL: testIsoceles (__main__.ClassifyTriangleTest) ---------------------------------------------------------------------- Traceback (most recent call last): File "basic_triangle_test.py", line 15, in testIsoceles self.assertEquals(result, 'isoceles') AssertionError: 'scalene' != 'isoceles' ---------------------------------------------------------------------- Ran 3 tests in 0.000s FAILED (failures=1)

Alright! This is the first bug that our test suite has caught! In the *classify_triangle* routine, we’re not correctly checking all three cases where we might have an isoceles triangle. Here’s the corrected method:

def classify_triangle(a, b, c): if a == b == c: return 'equilateral' elif (a == b) or (b == c) or (a == c): return 'isoceles' ######## else: return 'scalene'

I’ve added in the new check. Lets run the unit tests again:

python basic_triangle_test.py ... ---------------------------------------------------------------------- Ran 3 tests in 0.000s OK

Great success!

The next bug in *classify_triangle* is a bit more subtle. There’s a fourth case that wasn’t specified in the original problem statement: what if the numbers specified don’t represent a triangle at all?

Here’s another quick refresher on triangles (pardon the overly mathematic nature of it):

If A, B, C represent the lenghts of the sides of a triangle, and A ≤ B ≤ C, then A + B > C if (and only if) the lengths represent a valid triangle.

Off the top of my head, here’s two examples that aren’t valid:

- (1, 1, 10) will be classified as
*isoceles*, but isn’t really a triangle at all - (1, 2, 5) will be classified as
*scalene*, but isn’t really a triangle either.

Lets add two test cases for these:

def testInvalidIsoceles(self): result = classify_triangle(1, 1, 10) self.assertEquals(result, 'notvalid') def testInvalidScalene(self): result = classify_triangle(1, 2, 5) self.assertEquals(result, 'notvalid')

And re-running the tests fails, as expected:

$ python basic_triangle_test.py .FF.. ====================================================================== FAIL: testInvalidIsoceles (__main__.ClassifyTriangleTest) ---------------------------------------------------------------------- Traceback (most recent call last): File "basic_triangle_test.py", line 26, in testInvalidIsoceles self.assertEquals(result, 'notvalid') AssertionError: 'isoceles' != 'notvalid' ====================================================================== FAIL: testInvalidScalene (__main__.ClassifyTriangleTest) ---------------------------------------------------------------------- Traceback (most recent call last): File "basic_triangle_test.py", line 30, in testInvalidScalene self.assertEquals(result, 'notvalid') AssertionError: 'scalene' != 'notvalid' ---------------------------------------------------------------------- Ran 5 tests in 0.001s FAILED (failures=2)

There’s a number of ways to address this in the original function, but I’d like to modify it so that it matches the description above (that is, sorting so that a ≤ b ≤ c, and performing the computation):

def classify_triangle(a, b, c): a, b, c = sorted([a, b, c]) if not (a + b) > c: return 'notvalid' if a == b == c: return 'equilateral' elif (a == b) or (b == c) or (a == c): return 'isoceles' else: return 'scalene'

Re-running the same set of tests now succeeds:

$ python basic_triangle_test.py ..... ---------------------------------------------------------------------- Ran 5 tests in 0.000s OK

There’s still more tests that should be done, but they’re not particularly interesting:

- We should try all three permutations for the invalid triangles.
- What if a, b, or c are negative?

Hopefully this is enough to get you going on writing simple tests with Python and the *unittest* framework. If there’s details that aren’t clear, please post about them below.

tony arkles blog » Testing Part 2: Basic Python Unit Testingsaid on February 15, 2009 at 1:57 pm[...] Part 3 [...]

Evelynsaid on March 27, 2009 at 10:36 amYou’ve got a typo in the ‘notvalid’ triangle test. The html code for gt should be the symbol.

I like this series. It’s stepping through the process clearly.

My only criticism, and it’s one I don’t know how to address, is that some people find math examples hard to comprehend. I think that’s because computer people pick them because they believe the example has a strong shared truth value. So, they don’t have to spend as much time explaining or setting up the example. Plus, non-math examples, you might just argue over what it means, or is it true.

But, I think this false, code, test, fix cycle is especially valuable in problem spaces where there isn’t a strong shared understanding because it gives you the chance to learn about both the problem and the solution in little steps.

Thanks!