Categories
Blog Software Development

Test Driven Development – A practical Example

Step Nine

In our last step, we postponed multiplication of multiple and division numbers. Looking at the code Calculator, you might say that this should already work now. There is no distinction between multiplication/division and addition/subtraction. You are right, but this is exactly the problem. The code does not take operator precedence into account. This means, an expression containing both addition or subtraction and multiplication or division could fail. This is because our code is working from left to right.

Integration Tests

Let’s put this analysis into new integration tests. We start with multiplication and division of multiple numbers:

def test_multiplication_of_multiple_numbers(self):
    output = self.app.calculate("3 * 5 * 2")
    self.assertEqual(output, "30")

    output = self.app.calculate("0 * 5 * 7 * 1231")
    self.assertEqual(output, "0")

def test_division_of_multiple_numbers(self):
    output = self.app.calculate("3 / 5 / 2")
    self.assertEqual(output, "0.30")

    output = self.app.calculate("0 / 5 / 7")
    self.assertEqual(output, "0.00")

    output = self.app.calculate("1.5 / -1 / 3 / -1")
    self.assertEqual(output, "0.50")

As we expected, these tests are green. We don’t need to implement anything to make this work.

But what about mixed operations? Let’s write tests for them:

def test_mixed_operations(self):
    output = self.app.calculate("3 + 5 * 2")
    self.assertEqual(output, "13")

    output = self.app.calculate("10 + 5 / 2")
    self.assertEqual(output, "12.50")

    output = self.app.calculate("3 * 5 + 2")
    self.assertEqual(output, "17")

Which result should we expect from the first operation, knowing the operators are evaluated only from left to right? Instead of the correct result “13” we should expect “16”. We let our test prove it:

FAIL: test_mixed_operations (testIntegrationCalculatorApplication.TestIntegrationCalculatorApplication)
----------------------------------------------------------------------
Traceback (most recent call last):
  File ".../testIntegrationCalculatorApplication.py", line 147, in test_mixed_operations
    self.assertEqual(output, "13")
AssertionError: '16' != '13'
- 16
+ 13

We were right – 16! Our next task is to figure out how to implement mixed operations.

Implementing Mixed Operations

Where do we need to change our program to make mixed operations work? Tokenizer does not need to know about operator precedence. Also, ExpressionValidator does not need to change, because it accepts the mixed operations without problems. The problem lies within Calculator, which always calculates the terms from left to right. Calculator needs a kind of preprocessing step in which it determines the order of the terms to calculate. Before we decide how the preprocessing looks like, we add unit tests for Calculator with mixed operations. Later, these tests will show us that we did the preprocessing right.

def test_mixed_operations(self):
    result = self.calculator.calculate([Number(3), Operator.Addition, Number(5), Operator.Multiplication, Number(2)])
    self.assertEqual(result, 13)

    result = self.calculator.calculate([Number(3), Operator.Division, Number(6), Operator.Subtraction, Number(2)])
    self.assertEqual(result, -1.5)

Designing a Term Building Algorithm

Now we have a failing unit test. Let’s design the algorithm for building a term. It will need to create a tree of terms. Then the calculation can start with the root term. Each operand in that term can either be a number or another term. In case of a term, the result of that term needs to be calculated before the current term can be calculated. This means we will end up with an iterative algorithm.

To build the tree of terms, we can use the following algorithm:

  • Take the first three tokens and create a term containing them.
  • Check the next token and compare its precedence with the operator of the current term.
    • If the next token has a higher precedence, then replace the right hand side of the current term with a new term containing the right hand side, the operator and the next number.
    • If the next token has the same precedence, build a new term with the current term the left hand side and the new number the right hand side.
  • If all tokens have been processed, return the current term, which is the root term.

Introducing a Term Class

Our algorithm will need to check if an operand is a term or a number. We already have a class that represents a number, but we need a class to represent a term. It shall accept three parameters in the initializer. The first one and third need to be either of type Number or Term, and the second one needs to be an Operator. Let’s test for that:

class TestTerm(unittest.TestCase):
    def test_getters(self):
        term1 = Term(Number(0), Operator.Addition, Number(1))
        self.assertEqual(term1.get_operand_1(), Number(0))
        self.assertEqual(term1.get_operator(), Operator.Addition)
        self.assertEqual(term1.get_operand_2(), Number(1))

        term2 = Term(term1, Operator.Subtraction, Number(2))
        self.assertEqual(term2.get_operand_1(), term1)
        self.assertEqual(term2.get_operator(), Operator.Subtraction)
        self.assertEqual(term2.get_operand_2(), Number(2))

The implementation of Term looks like this:

class Term:
    def __init__(self, operand_1, operator, operand_2):
        self.operand_1 = operand_1
        self.operator = operator
        self.operand_2 = operand_2

    def get_operand_1(self):
        return self.operand_1

    def get_operand_2(self):
        return self.operand_2

    def get_operator(self):
        return self.operator

Operator Precedence

Another prerequisite of our algorithm is that we can check for the priority of operators. It makes sense to check this in Operator itself. Let’s add a test that checks all possible combinations of operators:

class TestOperator(unittest.TestCase):
    def test_operator_precedence(self):
        self.assertFalse(Operator.Addition.has_higher_precedence_than(Operator.Addition))
        self.assertFalse(Operator.Addition.has_higher_precedence_than(Operator.Subtraction))
        self.assertFalse(Operator.Addition.has_higher_precedence_than(Operator.Multiplication))
        self.assertFalse(Operator.Addition.has_higher_precedence_than(Operator.Division))

        self.assertFalse(Operator.Subtraction.has_higher_precedence_than(Operator.Addition))
        self.assertFalse(Operator.Subtraction.has_higher_precedence_than(Operator.Subtraction))
        self.assertFalse(Operator.Subtraction.has_higher_precedence_than(Operator.Multiplication))
        self.assertFalse(Operator.Subtraction.has_higher_precedence_than(Operator.Division))

        self.assertTrue(Operator.Multiplication.has_higher_precedence_than(Operator.Addition))
        self.assertTrue(Operator.Multiplication.has_higher_precedence_than(Operator.Subtraction))
        self.assertFalse(Operator.Multiplication.has_higher_precedence_than(Operator.Multiplication))
        self.assertFalse(Operator.Multiplication.has_higher_precedence_than(Operator.Division))

        self.assertTrue(Operator.Division.has_higher_precedence_than(Operator.Addition))
        self.assertTrue(Operator.Division.has_higher_precedence_than(Operator.Subtraction))
        self.assertFalse(Operator.Division.has_higher_precedence_than(Operator.Multiplication))
        self.assertFalse(Operator.Division.has_higher_precedence_than(Operator.Division))

The method has_higher_precedence_than looks like this:

class Operator(Enum):
    Addition = 1
    Subtraction = 2
    Multiplication = 3
    Division = 4

    def has_higher_precedence_than(self, other_operator):
        if(other_operator == Operator.Addition or other_operator == Operator.Subtraction) and (self == Operator.Multiplication or self == Operator.Division):
            return True
        else:
            return False

Term Building

Now we can begin implementing the term building method. The resulting code looks as follows:

class Calculator:
    def calculate(self, tokens):
        if len(tokens) < 3:
            return 0

        # Build first root term
        root_term = Term(*tokens[0:3])
        rest = tokens[3:]

        # Consume expression from left to right and update the root term
        while(type(rest) is list and len(rest) > 0):
            root_term = self.build_term(root_term, *rest[0:2])
            rest = rest[2:]

        # Calculate the result of the expression, starting with the root term
        return self.calculate_term(root_term).get_value()

    def build_term(self, operand_1, operator, operand_2):
        if type(operand_1) is Term:
            if operator.has_higher_precedence_than(operand_1.get_operator()):
                # Rebuild term to reflect higher operator precedence
                term_1 = Term(operand_1.get_operand_2(), operator, operand_2)
                term_2 = Term(operand_1.get_operand_1(), operand_1.get_operator(), term_1)
                return term_2

        # No operator precedence, build new term from left to right
        term = Term(operand_1, operator, operand_2)
        return term


    def calculate_term(self, term):
        operand_1 = term.get_operand_1()
        operator = term.get_operator()
        operand_2 = term.get_operand_2()

        # Calculate operands recursively until we have a Number
        while type(operand_1) is Term:
            operand_1 = self.calculate_term(operand_1)

        while type(operand_2) is Term:
            operand_2 = self.calculate_term(operand_2)

        # Calculate result
        if operator == Operator.Addition:
            sum = operand_1 + operand_2
            return sum
        if operator == Operator.Subtraction:
            sub = operand_1 - operand_2
            return sub
        if operator == Operator.Multiplication:
            mul = operand_1 * operand_2
            return mul
        if operator == Operator.Division:
            div = operand_1 / operand_2
            return div

        return Number(0)

This code makes all our test cases pass.

It works in three steps:

  • Build the first root term from the first three tokens.
  • Update the root term by taking the current root term and the next two tokens.
    • If the next operator has a higher precedence then the current root term, update the root term by creating a new term for the new operator. This will give this operator a higher precendence because its result will be calculated first.
    • Otherwise create a new term with the current root term as the first operator. Thus the current root term will be calculated first.
  • Calculate the result of the expression. Start with the current root term.
    • Extract operands and operator.
    • If an operand is a Term, call calculate_term recursively until the result is a Number.
    • Calculate result by performing the operation specified by the Operand.

Overloading of Arithmetic Operators

You might have noticed that the implementation of calculate_term had been changed to return a Number instead of an intor a float. This was done so that calculate_term can be called recursively. This way, no conversion between Number and an integral data type was needed. This conversion is at the end of the calculation, in the last line of calculate.

As all calculations in this method are done with Numbers, it became necessary to overload the arithmetic operators for Number. This avoids having to call get_value multiple times in calculate_term, which clutters the code. Instead, we do it in the implementation of Number, which now looks like this:

class Number():
    def __init__(self, value):
        self.value = value

    def __eq__(self, other):
        return self.value == other.value

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

    def __add__(self, other):
        return Number(self.get_value() + other.get_value())

    def __sub__(self, other):
        return Number(self.get_value() - other.get_value())

    def __mul__(self, other):
        return Number(self.get_value() * other.get_value())

    def __truediv__(self, other):
        return Number(self.get_value() / other.get_value())

    def get_value(self):
        return self.value

It is not necessary to add unit tests for the overloaded operators, because the implementation is very simple. It is thoroughly tested by the unit tests for calculate, which calls all of Number‘s methods. More unit tests would not add any more value here.

With all the tests green and the refactoring of Number completed, we can conclude the implementation of operator precedence.

Summary of Step Nine

In this step, we successfully implemented mixed operations. We did it by formalizing the concept of terms in a corresponding class. The challenge was to find an algorithm that builds a term tree while taking operator precedence into account. Once we formalized the algorithm, we first implemented the helper methods that we needed to write the algorithm in a convenient way. Then it was relatively easy to implement the algorithm and make the tests pass.

After successfully implementing mixed operations, only one item is left: Bracketed expressions. We will implement them in Step Ten.

Leave a Reply

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