Decision Table: A table where each row represents a test case. A column can contain either a test input or an expectation. The rows can be executed either sequential or in parallel.
value a | value b | a + b = ? |
---|---|---|
1 |
2 |
3 |
-1 |
2 |
1 |
In this example the columns value a
and value b
are test inputs and the column
a + b = ?
is an expectation.
The execution of a decision table follows a very specific life cycle. It is comparable to the phases of a unit test class:
-
before table
-
create fixture instance (parallel or not)
-
before row
-
set inputs
-
before first check
-
execute checks
-
after row
-
after table
Given the example at the beginning of this chapter, the execution would be as follows:
-
execute
before table
methods -
create new fixture instance (parallel or not)
-
execute
before row
methods -
set input matching
value a
to1
-
set input matching
value b
to2
-
execute
before first check
methods -
execute check matching
a + b = ?
with expectation3
-
execute
after row
methods -
create new fixture instance
-
execute
before row
methods -
set input matching
value a
to-1
-
set input matching
value b
to2
-
execute
before first check
methods -
execute check matching
a + b = ?
with expectation1
-
execute
after row
methods -
execute
after table
methods
When executing a decision table, there are multiple points where exceptions can occur.
-
An exception in a
before table
method will stop the table from being executed with the exception ofafter table
methods. -
An exception in a
before row
method will stop the row from being executed with the exception ofafter row
methods. -
An exception in
set input
will stop the row from being executed any further with the exception ofafter row
methods. -
An exception in a
before first check
method will stop the row from being executed any further with the exception ofafter row
methods. -
An exception in a
execute check
method will fail the check but continue the row execution.
LivingDoc distinguishes exceptional cases between AssertionError
and any
other Exception
:
The occurrence of an AssertionError
in set input
or execute check
will
result in the this step being marked as failed
. The occurrence of any other
Exception
will mark it with exception
.
The occurrence of any Exception
in before row
, before first check
or
after row
will mark the entire row with exception
. As will the occurrence
of any Exception
in before table
or after table
mark the entire table
with exception
.
This annotation is used to declare a class as a decision table fixture that contains all necessary steps.
The annotation allows the specification of a parameter parallel
with a boolean. The parameter is set to false
by default. That means the rows of the decision table fixture will executed in sequential order.
If parallel
is set to true, the rows will be executed in parallel.
@DecisionTableFixture(parallel = true)
class ParallelDecisionTableFixture {
...
}
@DecisionTableFixture(parallel = false)
class SequentialDecisionTableFixture {
...
}
or
@DecisionTableFixture
class SequentialDecisionTableFixture {
...
}
The annotated class must be static.
This annotation is used on static methods in order to bind them to the life
cycle phase before table
. The annotated method must be static and is not
allowed to have any parameters.
These methods are generally used to execute setup code only once before the table is evaluated. This could be the initialization of a browser or start of a server. Basically any expensive required setup.
Multiple methods can be bound by this annotation. The order in which they are executed is non-deterministic. As a general rule there should not be any chronological dependency between these methods.
@BeforeTable
static void createTestData() {
...
}
This annotation is used on instance methods in order to bind them to the life
cycle phase before row
. The annotated method must not be static and is
not allowed to have any parameters.
These methods are generally used to execute setup code for each test case. As an example, this could be the reset of the system under test.
Multiple methods can be bound by this annotation. The order in which they are executed is non-deterministic. As a general rule there should not be any chronological dependency between these methods.
@BeforeRow
void resetState() {
...
}
This annotation is used on instance fields or methods in order to set a test input value on the fixture. If it is used on a method, this method must not be static and must have exactly one parameter.
For most test inputs it should be enough to annotate the corresponding field. Input methods are often used when additional logic is required to set the actual value (e.g. loading a value from a database).
This annotation can be used multiple times on the same target in order to define additional mappings for this test input. This can be useful if multiple languages or alternative spelling need to be supported.
@Input("value a")
void setValueA(Double valueA) {
this.valueA = valueA;
}
@Input("value b")
Double valueB;
This annotation is used on instance methods in order to bind them to the life
cycle phase before first check
. The annotated method must not be static
and is not allowed to have any parameters.
These methods are generally used in order to gather same data used by the checks in case gathering that data inside the check methods would take too long. This is often the case when the fixture is based on UI-Interactions.
Multiple methods can be bound by this annotation. The order in which they are executed is non-deterministic. As a general rule there should not be any chronological dependency between these methods.
@BeforeFirstCheck
void gatherDataForChecks() {
...
}
This annotation is used on instance methods in order to bind that method to an expectation column of the decision table. The annotated method must not be static and must have exactly one parameter.
These methods must execute some assertion logic to verify that the actual value resulting from some action is equal to the expressed expectation provided as the parameter of the method.
This annotation can be used multiple times on the same target in order to define additional mappings for this check. This can be useful if multiple languages or alternative spelling need to be supported.
@Check("a + b = ?")
void checkSum(Double expectedValue) {
Double actualValue = ...
assertThat(actualValue).isEqualTo(expectedValue);
}
This annotation is used on instance methods in order to bin them to the life
cycle phase after row
. The annotated method must not be static and is
not allowed to have any parameters.
These methods are generally used to cleanup whatever was created in a
corresponding @BeforeRow
method.
Multiple methods can be bound by this annotation. The order in which they are executed is non-deterministic. As a general rule there should not be any chronological dependency between these methods.
@AfterRow
void resetState() {
...
}
This annotation is used on static methods in order to bind them to the life
cycle phase after table
. The annotated method must be static and is not
allowed to have any parameters.
These methods are generally used to cleanup whatever was created in a
corresponding @BeforeTable
method.
Multiple methods can be bound by this annotation. The order in which they are executed is non-deterministic. As a general rule there should not be any chronological dependency between these methods.
@AfterTable
void deleteTestData() {
...
}
link:{moduleBase}/src/main/java/examples/decisiontables/CalculatorFixture.java[role=include]
The following tables lists some benchmark results. The benchmarks were run on an i5-5200U with 8GB of RAM.
Benchmark |
Execution Mode |
Number of Rows |
Average Time (ms) |
Simple Operation |
sequential |
1,000 |
13.111 |
Simple Operation |
sequential |
1,000,000 |
14456.335 |
Simple Operation |
parallel |
1,000 |
5.456 |
Simple Operation |
parallel |
1,000,000 |
8900.850 |
Expensive Operation |
sequential |
10 |
7987.379 |
Expensive Operation |
parallel |
10 |
4845.735 |