Categories
Blog Software Development

Test Driven Development – A practical Example

Step Six

Our list has shrunk again. Now it looks like this:

  • Addition of negative numbers
  • Addition of multiple numbers
  • Multiplication
  • Division
  • Bracketed Expressions

In this step we are going to implement negative numbers. Let’s start with new use cases in the integration test:

output = self.app.calculate("-120 + 80")
self.assertEqual(output, "-40")

output = self.app.calculate("120 + -80")
self.assertEqual(output, "40")

output = self.app.calculate("-120.5+ 80.6")
self.assertEqual(output, "-39.9")

output = self.app.calculate("-120- 80")
self.assertEqual(output, "-200")

output = self.app.calculate("-120- -80")
self.assertEqual(output, "-40")

output = self.app.calculate("120--80")
self.assertEqual(output, "200")

There are tests for several combinations of signs, operators and whitespace – even very tricky combinations, like the operator directly followed by the sign. These should also work, so we specifiy a valid result for them, too.

At this stage, three of our new integration tests fail.

Improving Tokenization

To start with the implementation, we first need to enhance Tokenizer: it can not read the negative sign of numbers yet. As soon as it can detect negative numbers and produce instances of Number with negative values, we can expect the integration tests to be green again.

As a step in this direction, we write a unit test for Tokenizer. It has several examples for valid strings with negative numbers, and also one for a string in which operator and sign directly follow each other.

def test_tokenize_string_with_negative_numbers(self):
    output = self.tokenizer.tokenize("-120+ 8")
    self.assertEqual(output, [Number(-120), Operator.Addition, Number(8)])

    output = self.tokenizer.tokenize("120+ -8")
    self.assertEqual(output, [Number(120), Operator.Addition, Number(-8)])

    output = self.tokenizer.tokenize("120--8")
    self.assertEqual(output, [Number(120), Operator.Subtraction, Number(-8)])

Creating a Helpful Error Message

This test fails as expected. But the error message is not really helpful:

======================================================================
FAIL: test_tokenize_string_with_negative_numbers (testTokenizer.TestTokenizer)
----------------------------------------------------------------------
Traceback (most recent call last):
  File ".../testTokenizer.py", line 27, in test_tokenize_string_with_negative_numbers
    self.assertEqual(output, [Number(-120), Operator.Addition, Number(8)])
AssertionError: Lists differ: [<Operator.Subtraction: 2>, <Number.Number [81 chars]438>] != [<Number.Number object at 0x108373978>, <Op[54 chars]7f0>]

First differing element 0:
<Operator.Subtraction: 2>
<Number.Number object at 0x108373978>

This error message does not help us determine which number was actually expected. It is only the address of the object. This makes the error output hard to read and even harder to find the source of the error. We need to take care of this before fixing the unit test itself. Otherwise the error message of the unit test will not help us finding the problem if we break the test in the future.

Instead of the address of the object, the test should print the value of the Number object. We can implement this easily by implementing the special method repr in Number:

def __repr__(self):
    return "Number with value: " + str(self.value)

Now the error message has become comprehensible:

FAIL: test_tokenize_string_with_negative_numbers (testTokenizer.TestTokenizer)
----------------------------------------------------------------------
Traceback (most recent call last):
  File ".../testTokenizer.py", line 27, in test_tokenize_string_with_negative_numbers
    self.assertEqual(output, [Number(-120), Operator.Addition, Number(8)])
AssertionError: Lists differ: [<Operator.Subtraction: 2>, Number with value: 8] != [Number with value: -120, <Operator.Addition: 8>]

First differing element 0:
<Operator.Subtraction: 2>
Number with value: -120

First list contains 1 additional elements.
First extra element 3:
Number with value: 8

+ [Number with value: -120, <Operator.Addition: 1>, Number with value: 8]
- [<Operator.Subtraction: 2>,
-  Number with value: 120,
-  <Operator.Addition: 1>,
-  Number with value: 8]

It shows us that the list of tokens contains four elements instead of the expected three. The resulting list has a subtraction operator at the beginning, which we do not expect there. Apparently the Tokenizer detected the sign of the first number as an operator.

Updating the Regular Expression

Now we need to find an extension of the regular expression that captures the sign of a Number.

It turns out we can find a proper expression easily. We just need to capture an optional ‘-‘ before any number:

expr = re.compile(r"-?\d+.\d+|-?\d+|[+*/\-]")

This change makes all our test cases for Tokenizer green again. However, one integration test still fails.

======================================================================
FAIL: test_subtraction_of_two_numbers (testIntegrationCalculatorApplication.TestIntegrationCalculatorApplication)
----------------------------------------------------------------------
Traceback (most recent call last):
  File ".../testIntegrationCalculatorApplication.py", line 79, in test_subtraction_of_two_numbers
    self.assertEqual(output, "0")
AssertionError: 'Invalid expression' != '0'
- Invalid expression
+ 0

The failing check looks like this:

output = self.app.calculate("0 -0")
self.assertEqual(output, "0")

This test had been successful before the changes. But now it is failing. Why? Naturally, it should fail, as the operator is missing! In this string, the ‘-‘ is the sign of the second number. Thus the operator is missing. The test does not make sense this way.

Let’s add a space between the ‘-‘ and the 0 to make it a valid string again. And this makes the test pass again.

Summary of Step Six

In this step, we have implemented negative numbers.

We added a method to print instances of Number to make the error message of our test comprehensible. This will help us in the future, should we ever break the test again. Thus it is very important to make the error messages of our tests as helpful as possible. We can do this most easily by looking at the error message when the test is still red.

From this message, we figured out in which way Tokenizer behaved in our new scenario. The error message helped us to find out how to adapt the regular expression.

In the next step, we are going to implement the missing operations: Multiplication and Division.

Leave a Reply

Your email address will not be published. Required fields are marked *