Rule Validation (Decision Table)


Definition

Definition

The DecisionTableInterpreter is used to express concrete and measurable business rules.
During the execution of the specification,  compares the values returned by the system under development against the expected values defined by the Business Expert.

  • The first row of the table indicates the set of rules to be tested by .
  • The next row is called the header row and serves to distinguish the given values and the expected values. Given values serve as inputs to the system, whereas expected values serve as comparison values against values that are actually returned by the system. When a column header ends with special characters ? or (), it denotes an expected value.
  • Finally, the remaining rows capture the examples. They are the executable test rows.

Specific keywords for expected values

 offers a list of useful keywords to support the Business Expert.

Empty cells
When a test cell is left blank,  only shows the returned value
errorWhen you expect an error, specify it in the cell to test that particular behavior

Coloring

 will visually show the test result by coloring each testing cell:

Green

 When the expected value matches the returned value, the DecisionTableInterpreter colors the cell as "right" by coloring it green.

Red

If the values don't match, the DecisionTableInterpreter colors the cell as "wrong" in red.

YELLOW

If the system encounters an execution error, the cell is colored yellow and  provides information about the error.

Grey

If no expected value is specified in a test, the DecisionTableInterpreter colors the cell in gray and displays the returned value.

Here is an example of cell coloring:

Input Table:

Decision TableCalculator
xysum?product()quotient?
1001000
639-5

Output Table:

Decision TableCalculcator
xysum?product()quotient?
100100

0


java.lang.ArithmeticException: / by zero
info.novatec.testit.livingdoc.fixture.calculator.Calculator.quotient(Calculator.java:63)
639Expected: -5 Received: 182

Writing fixtures for Rule tables

As we've seen in the Rule For Definition, a table of rules is used to express business rules of the application under development.

A fixture for a table of rules defines how the specific given and expected columns of a rule table are mapped to the system under development.

This page shows the fixture code that supports the examples introduced in the Writing a Rule For Specification  documentation.

Fixture for Calculator

Consider the first example of business rule table described in Table of Rules documentation, shown again below.

Decision TableDivision
dividenddivisorquotient?
6.02.03.0
723.5
183.06
1527

The first cell of the first row indicates that a DecisionTableInterpreter will be used to interpret the example table. The next cell says that the fixture to use is called Division. In Java, the name of the fixture is the name of a Java class.

The second row, also know as the header row, designates the given columns and expected columns. In the example, dividend and divisor are given columns, whereas quotient? is an expected column.

The fixture code to support this example in Java is the class Calculator shown below.

Show me the code


Code for the Calculator fixture
public class Division
{
    public double dividend;
    public double divisor;

    public double quotient()
    {
        return dividend / divisor;
    }
}

That class follows the general rules of fixtures described in the Fixture Conventions. It exposes the instance variable dividend to map with the given column dividend. The given column divisor corresponds to the public instance variable divisor. The expected column quotient? is mapped to the public instance method quotient().

This is a very simple example in which the fixture object does not call on the system under development, but performs the calculation. The quotient() method uses the instance variables dividend and divisor to perform the calculation by itself.

How is the table processed?

When it runs this table,  reads the first row to select the interpreter and fixture. It then reads the second to know what are the given and expected columns. Finally it starts testing from the third row down to the end of the table.

For the third row  carries out the following sequence of steps:

  1. It assigns the value 6.0 to the dividend instance variable
  2. It assigns the value 2.0 to the divisor instance variable
  3. It calls the method quotient() of the fixture to get the value calculated by the system under development.
  4. It reads the value 3.0 from the quotient? column and compares it to the value returned by the fixture. Since the values are equal, it will annotate the cell as right, which results in the cell being colored green.

Same goes for the fourth and fifth rows.

On the other hand, on the last row the comparison will fail.  will mark the expected cell wrong. That cell will be colored red and will display a message including the expected value and the actual value.

How are the types of the values handled?

The instance variables dividend and divisor are of type double, so the given values in the example table needs to be double as well. For the fourth row,  will automatically convert the given values 7 and 2 to doubles before assigning them to the instance variables.

The return value of the quotient method is also a double, which means that the values provided in the quotient? expected column must be doubles as well. When  compares the value returned by the fixture with the value provided as an expectation, it will first convert the expected value to the type of the actual returned value. In the fifth row, it will convert the value 6 to a double and then do the comparison.

A More Realistic Example

The Calculator example is very simple. In a real world example, the fixture code would not perform any real work but would instead delegate to the application under development. In other words, if this was a real application the fixture would not carry out the division operation but call on the system under development. The general rule is to keep the fixture as thin as possible to be merely a mediator between the example table and the application code.

The Mortgage example described in Writing a Decision Table specification document better illustrates the role of the fixture and its interactions with the system under development. This example is shown again here:

Decision Tableinsurance premium fee calculation
sale pricedown paymentpremium fee?financed amount?
$100,000$15,000$2,125.00
$100,000$30,000$0.00
$100,000$25,000$0.00

This example uses the InsurancePremiumFeeCalculation fixture class. The two given columns, which are sale price and down payment, are mapped respectively to the instance variables salePrice and downPayment. The two expected columns, called premium fee? and financed amount?, correspond respectively to the methods premiumFee() and financedAmount().

How are spaces between words handled?

You have noticed that you can separate words in the example table with spaces.  will convert the sequence a space-separated words to a valid Java identifier by removing the spaces and capitalizing the first letter of every word except the first one. This is called camel casing.

  • For class names,  uses upper camel casing with the first letter of the identifier capitalized as well. In our example, the label insurance premium fee calculation is converted to the class name InsurancePremiumFeeCalculation.
  • For method or instance variable names,  uses lower camel casing where the first letter of the identifier is left in lowercase.

Show me the code

The supporting code is here:

Code for the InsurancePremiumFeeCalculation fixture
public class InsurancePremiumFeeCalculationFixture
{
    public Money salePrice;
    public Money downPayment;

    public Money premiumFee()
    {
        InsuranceFee insuranceFee = new InsuranceFee(salePrice);
        return insuranceFee.forDownpayment(downPayment);
    }

    public Money financedAmount()
    {
        return salePrice.minus(downPayment);
    }
}

You might have noticed that the fixture class name is InsurancePremiumFeeCalculationFixture and not InsurancePremiumFeeCalculation. It does not matter to . For fixture class names, the Fixture suffix can be added to make it easier to differentiate from the application under development code.  will deal with it automatically.

In this example, the fixture merely delegates the work to the application under development. Namely, the InsuranceFee domain object is responsible for performing the actual work.

The code for the system under development is:

Code for the system under development
public class InsuranceFee
{
    private final Money salePrice;

    public InsuranceFee(Money salePrice)
    {
        this.salePrice = salePrice;
    }

    public Money forDownPayment(Money downPayment)
    {
        return downPayment.greaterThan(salePrice.times(Ratio.percent(25))) ? Money.zero() : financedAmount( downPayment ).times( Ratio.of(25, 1000) );
    }

    private Money financedAmount(Money downPayment)
    {
        return salePrice.minus( downPayment );
    }
}

What about the empty column?

The financed amount? expected value is left empty to tell  that this is an information column. We don't want to perform any test in that column, but rather show the value to help our understanding of the calculation. The calculated values will be shown when the example is run and marked with the ignored annotation, making them appear gray.

How are application value types handled?

We've seen that  will automatically convert the given values in the table to the type of the corresponding instance variable.

This is done automatically for the most common built-in Java types. The mortgage example, however, uses a money type, which is defined by the application under development in the Money business domain class.

When  encounters a custom type while performing conversion, it will look for a public static method called parse to handle the conversion from a String to the custom type. To do the conversion from the custom type to a String, it will call the method toString() on the custom type.

When  verifies an expected value to an actual value for a custom type, it does the following steps. It:

  1. Converts the expected value specified as a string in the table to the custom type using the parse(String text) method.
  2. Compares the expected value and the actual value using the equals(Object other) method on the expected value, passing it the actual value.

Here's the relevant part of the code for the Money class:

Code for the Money class
public class Money
{
    private final BigDecimal dollars;

    public Money(BigDecimal dollars)
    {
        this.dollars = dollars;
    }

    public static Money parse(String text)
    {
        return new Money( new BigDecimal( normalize( text ) ) );
    }

    private static String normalize(String text)
    {
        return text.replaceAll( "\\$", "").replaceAll( ",", "").replaceAll( "\\s", "");
    }

    // ...

    public boolean equals(Object other)
    {
        if (other instanceof Money)
        {
            Money that = (Money) other;
            return this.dollars.compareTo( that.dollars ) == 0;
        }
        else
        {
            return false;
        }
    }

    public int hashCode()
    {
        return dollars.hashCode();
    }

    public String toString()
    {
        return "$" + dollars;
    }

    // ...
}

Row actions in a DecisionTable

There are 3 annotations(attributes) that can be used in the fixture with a DecisionTableInterpreter .

  • BeforeRow
  • AfterRow
  • BeforeFirstExpectation

BeforeRow

A method annotated with BeforeRow will be called at the beginning of each new row so you can prepare your fixture.

AfterRow

A method annotated with AfterRow will be called at the end of each row so you can reset your fixture.

BeforeFirstExpectation

The method annotated with BeforeFirstExpectation will be called on each row just before the first ExpectedColumn is encountered so you can prepare the return values for this row.

Code with RowAction annotations
 public class DomainStuffFixture
{
    private DomainStuff stuff;
    private int x;
    private int y;

    public void setX(int x) 
    {
         this.x = x;
    } 

    public void setY() 
    {
        this.y = y;
    }

    @BeforeFirstExpectation
    public void allocateDomainStuff() {
        stuff = DomainStuffService.allocateDomainStuff(x, y);
    }

    public int getComputedValueOne() 
    {
         return stuff.getComputedValueOne();
    }

    public int getOtherComputedValueOne() 
    {
         return stuff.getOtherComputedValue();
    }

    @AfterRow
    public void freeDomainStuff() {
        DomainStuffService.freeStuff(stuff);
    }

    // ...
}

The three annotations are optional, you can use none, some or all of them in your fixture depending on your needs.

Setup and Teardown methods for Decision tables

There are 2 annotations(attributes) that can be used in the fixture with a RuleForInterpreter.

  • BeforeTable
  • AfterTable

BeforeTable

A method annotated with BeforeTable works like the BeforeRow annotation but is called before each Table instead of each row.

AfterTable

A method annotated with AfterTable works like the AfterRow annotation but is called after each Table instead of each row.

Wrap Up

When  runs a rule table example, it creates a fixture class from the name indicated in the second cell of the first row. That class is used to mediate between the development table and the application code when checking the example against the system under development.

The second row of the table is the header row and is read from left to right. Two types of columns are supported in rule tables:

  • given values, which are mapped to public instance variables in the fixture class
  • expected values, which are checked against values returned by the corresponding public instance methods

Headers

For a given column header cell, :

  • figures out the public instance variable to use using camel casing rules.
  • if there is no instance variable that matches the label of the column header, it marks the cell with an error annotation, causing the cell to appear yellow and to display a stack trace of the error. That column will be ignored entirely.

For an expected column header cell:

  • It figures out the public instance method to use using camel casing rules.
  • If there is no such method or if it's not publicly accessible, it marks the cell with an error annotation, causing the cell to appear yellow and to display a stack trace of the error. That column will be ignored entirely.

Columns

 will then run all remaining rows one at a time, going through all cells in the row from left to right.

For a given column:

  • It converts the text entered in the cell to the type of the instance variable. To do that it uses either built-in converters (for basic Java types) or delegates to a method called parse (for domain types).
  • It assigns the converted value to the instance variable.
  • However, if type conversion fails, the cell is reported as an error.

For an expected column:

  • It converts the text entered in the cell to the return type of the instance method. To do that it uses either built-in converters (for basic Java types) or delegates to a method called parse (for domain types).
  • It compares the actual value to the converted value based on the definition of the equality for the type.
  • If the values match, it marks the cell with a right annotation causing it to appear green.
  • If the values do not match, it marks the cell with a wrong annotation so that it appears red and displays the expected and actual values.
  • However, if type conversion fails or if the method throws an exception the cell is reported as an error.