Categories
Blog Software Development

Test Driven Development – A practical Example

Step Eight

The list looks like this now:

  • Addition of multiple numbers
  • Bracketed Expressions

The next logical step would be to implement addition of multiple numbers. When this works, the subtraction of multiple numbers should work the same way. What about multiplication and division? This would be more difficult, as the precedence rules for the different operators need to be implemented, too. To keep this step simple, let’s delay the multiplication and division of multiple numbers to another step. We add multiplication and division to our list. In this step, we focus on addition and subtraction, where the order does not matter.

  • Addition/Subtraction of multiple numbers
  • Multiplication/Division of multiple numbers
  • Bracketed Expressions

Integration Tests

How will this new functionality fit into our design, that currently only supports two numbers? Again, we start with some integration tests:

def test_addition_and_subtraction_of_multiple_numbers(self):
    output = self.app.calculate("1 + 2 + 3 + 4")
    self.assertEqual(output, "10")

    output = self.app.calculate("-1 + 2 - -3 + 4")
    self.assertEqual(output, "8")

    output = self.app.calculate("-0.1 + 0 - 0.0 + 0.1 + 0")
    self.assertEqual(output, "0.00")

The result of the calculation is “Invalid Expression”. This shows that either the Tokenizer or the ExpressionValidatorcannot cope with expressions with multiple numbers.

Tokenization

Let’s examine Tokenizer with an extension of the unit test for multiple numbers:

def test_tokenize_string(self):
    output = self.tokenizer.tokenize("3 + 5")
    self.assertEqual(output, [Number(3), Operator.Addition, Number(5)])

    output = self.tokenizer.tokenize("5 * -")
    self.assertEqual(output, [Number(5), Operator.Multiplication, Operator.Subtraction])

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

    # NEW More than two numbers
    output = self.tokenizer.tokenize("120+ 8 - 5 + -6")
    self.assertEqual(output, [Number(120), Operator.Addition, Number(8), Operator.Subtraction, Number(5), Operator.Addition, Number(-6)])

This test does not fail. The Tokenizer already supports our new use case.

An Exploratory Unit Test

This means the error must come from ExpressionValidator. Let’s extend the unit tests for valid expressions:

def test_valid_expression(self):
    isValid = self.validator.validate([Number(3), Operator.Addition, Number(5)])
    self.assertTrue(isValid)

    isValid = self.validator.validate([Number(120), Operator.Addition, Number(8)])
    self.assertTrue(isValid)

    isValid = self.validator.validate([Number(0), Operator.Addition, Number(0)])
    self.assertTrue(isValid)

    isValid = self.validator.validate([Number(3), Operator.Subtraction, Number(5)])
    self.assertTrue(isValid)

    # NEW Expression with multiple numbers
    isValid = self.validator.validate([Number(3), Operator.Subtraction, Number(5), Operator.Addition, Number(-3)])
    self.assertTrue(isValid)

This extended test fails! This proves our theory that ExpressionValidator does not support multiple numbers yet. Of course, we could have found this out by looking into the code. But the new test shows for sure that it does not work at all. And the test will also tell us if the extension of ExpressionValidator that we are going to perform is working.

The key point here is that a good strategy to find out what a piece of code does or does not do is to write a unit test first. Because we use it to explore the capabilites of some piece of code, we can call it exploratory test. Writing such tests is always a good idea when you are working with somebody else’s code that you don’t really understand, or with your own code that you wrote some time ago. The test will tell you if the assumptions that you make about the code are correct. If the test fails, you can easily debug the test code to find the source of the error.

Validation of Longer Expressions

Now that we have a failing unit test, let’s check out what we have to do next. The current implementation of ExpressionValidator looks like this:

class ExpressionValidator:
    def validate(self, tokens):
        if(len(tokens) == 0):
            return True

        if(len(tokens) == 3):
            if(type(tokens[0]) is Number and type(tokens[2]) is Number and type(tokens[1]) is Operator):
                return True

        return False

The check is currently hard coded for an expression with exactly two numbers and one operator. To make it work for more tokens, it should check the following:

  • The number of tokens needs to be odd and at least three.
  • The first token needs to be a number.
  • Every second token needs to be an operator.

Let’s try an implementation of this rule:

class ExpressionValidator:
    def validate(self, tokens):
        if(len(tokens) == 0):
            return True

        if(len(tokens) >= 3 and len(tokens) % 2 == 1):
            even_elements = tokens[::2]
            odd_elements = tokens[1::2]
            if(all(isinstance(x, Number) for x in even_elements) and all(isinstance(x, Operator) for x in odd_elements)):
                return True

        return False

This implementation makes the unit test for a valid expression with multiple numbers pass. And the output of the integration test looks different now:

FAIL: test_addition_and_subtraction_of_multiple_numbers (testIntegrationCalculatorApplication.TestIntegrationCalculatorApplication)
----------------------------------------------------------------------
Traceback (most recent call last):
  File ".../testIntegrationCalculatorApplication.py", line 96, in test_addition_and_subtraction_of_multiple_numbers
    self.assertEqual(output, "10")
AssertionError: '0' != '10'
- 0
+ 10
? +

The result of the calculation is now “0” instead of “Invalid Expression”. This means that we have made some progress.

Addition of Multiple Numbers

The next step is to implement the addition of more than two numbers in Calculator. We need some unit tests for this use case.

def test_addition(self):
    result = self.calculator.calculate([Number(3), Operator.Addition, Number(5)])
    self.assertEqual(result, 8)

    result = self.calculator.calculate([Number(120), Operator.Addition, Number(80), Operator.Addition, Number(100)])
    self.assertEqual(result, 300)

def test_subtraction(self):
    result = self.calculator.calculate([Number(3), Operator.Subtraction, Number(5)])
    self.assertEqual(result, -2)

    result = self.calculator.calculate([Number(120), Operator.Subtraction, Number(80), Operator.Subtraction, Number(100)])
    self.assertEqual(result, -60)

def test_mixed_addition_and_subtraction(self):
    result = self.calculator.calculate([Number(120), Operator.Addition, Number(80), Operator.Subtraction, Number(100)])
    self.assertEqual(result, 100)

We can use the existing tests test_addition_of_two_numbers and test_subtraction_of_two_numbers and extend one of the examples with more numbers. As these unit tests cover the more general case now, we can ommit the suffix of_two_numbers.

To cover the case of an expression with both addition and subtraction, we add another test case test_mixed_addition_and_subtraction. Of course, all three tests for Calculator fail now, as the method returns 0 whenever it is provided with an expression that it cannot calculate. Let’s change that.

class Calculator:
    def calculate(self, tokens):
        # Calculate first term
        first_term = tokens[0:3]
        rest = tokens[3:]
        result = self.calculate_term(first_term)

        # Build new term with the rest of the expression and calculate the result
        while(type(rest) is list and len(rest) > 0):
            next_term = [Number(result), *rest[0:2]]
            rest = rest[2:]
            result = self.calculate_term(next_term)

        return result


    def calculate_term(self, term):
        if(len(term) == 3):
            operator = term[1]
            if operator == Operator.Addition:
                sum = term[0].get_value() + term[2].get_value()
                return sum
            if operator == Operator.Subtraction:
                sub = term[0].get_value() - term[2].get_value()
                return sub
            if operator == Operator.Multiplication:
                mul = term[0].get_value() * term[2].get_value()
                return mul
            if operator == Operator.Division:
                div = term[0].get_value() / term[2].get_value()
                return div
        return 0

Let’s examine this solution in more detail. The original implementation of calculate has been moved into the method calculate_term. It takes a term, which in our case is a triple of two numbers and an operator, and returns the result of this term.

The implementation of calculate splits the token list into terms. It calculates the result of the first term. If there are more tokens in the expression, then it will build a new term with the result of the preceeding term and the following operator and number. It will continue until all tokens have been processed.

As a byproduct of this implementation, also multiplication and division of multiple numbers work now, as calculate_termalso supports these operators. But there is no notion of operator precedence yet, so mixing addition/subtraction with multiplication/division would give an invalid result.

Summary of Step Eight

In this step, we have implemented expressions with multiple numbers. We focused on the case of addition and subtraction, where we don’t have to take operator precedence into account. We postponed this to a following step by putting it on our list.

We started with an integration test that showed an error. We used exploratory unit tests to search for the cause of the error. They showed us that Tokenizer already worked well for our new use case, but ExpressionValidator rejected expressions with more than three numbers.

We implemented a generalized version of calculate. As a byproduct, it also supports multiplication and division of multiple numbers, but it does not know about operator precedence yet. This is what we implement in our next step.

Leave a Reply

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