With the fire-control system in place, you’re tasked with a more complex testing assignment. This time, the application under test is a monitor system for a nuclear reactor. It functions in a similar way to the fire monitor, but with more input sensors.
The system6 is shown in figure 3.4.
The components of the system are as follows:
■ Multiple fire sensors (input)
■ Three radiation sensors in critical points (input)
■ Current pressure (input)
■ An alarm (output)
5 See JBehave (http://jbehave.org/) or Cucumber JVM (http://cukes.info/) to see how business analysts, tes- ters, and developers can define the test scenarios of an enterprise application.
6 This system is imaginary. I’m in no way an expert on nuclear reactors. The benefits of the example will become evident in the mocking/stubbing section of the chapter.
Shutdown command
Fire sensors Radiation sensors
Alarm
Pressure
Evacuation timer
Processing unit
Figure 3.4 A monitor system for a nuclear reactor
71 Handling tests with multiple input sets
■ An evacuation command (output)
■ A notification to a human operator that the reactor should shut down (output) The system is already implemented according to all safety requirements needed for nuclear reactors. It reads sensor data at regular intervals and depending on the read- ings, it can alert or suggest corrective actions. Here are some of the requirements:
■ If pressure goes above 150 bars, the alarm sounds.
■ If two or more fire alarms are triggered, the alarm sounds and the operator is notified that a shutdown is needed (as a precautionary measure).
■ If a radiation leak is detected (100+ rads from any sensor), the alarm sounds, an announcement is made that the reactor should be evacuated within the next min- ute, and a notification is sent to the human operator that a shutdown is needed.
You speak with the technical experts of the nuclear reactor, and you jointly decide that a minimum of 12 test scenarios will be examined, as shown in table 3.1.
The scenarios outlined in this table are a classic example of parameterized tests. The test logic is always the same (take these three inputs and expect these three outputs), and the test code needs to handle different sets of variables for only this single test logic.
In this example, we have 12 scenarios with 6 variables, but you can easily imagine cases with much larger test data. The naive way to handle testing for the nuclear reactor
Table 3.1 Scenarios that need testing for the nuclear reactor
Sample inputs Expected outputs
Current pressure
Fire sensors
Radiation sensors
Audible alarm
A shutdown is needed
Evacuation within x minutes
150 0 0, 0, 0 No No No
150 1 0, 0, 0 Yes No No
150 3 0, 0, 0 Yes Yes No
150 0 110.4 ,0.3, 0.0 Yes Yes 1 minute
150 0 45.3 ,10.3, 47.7 No No No
155 0 0, 0, 0 Yes No No
170 0 0, 0, 0 Yes Yes 3 minutes
180 0 110.4 ,0.3, 0.0 Yes Yes 1 minute
500 0 110.4 ,300, 0.0 Yes Yes 1 minute
30 0 110.4 ,1000, 0.0 Yes Yes 1 minute
155 4 0, 0, 0 Yes Yes No
170 1 45.3 ,10.f, 47.7 Yes Yes 3 minutes
would be to write 12 individual tests. That would be problematic, not only because of code duplication, but also because of future maintenance. If a new variable is added in the system (for example, a new sensor), you’d have to change all 12 tests at once.
A better approach is needed, preferably one that decouples the test code (which should be written once) from the sets of test data and expected output (which should be written for all scenarios). This kind of testing needs a framework with explicit sup- port for parameterized tests.
Spock comes with built-in support for parameterized tests with a friendly DSL7 syn- tax specifically tailored to handle multiple inputs and outputs. But before I show you this expressive DSL, allow me to digress a bit into the current state of parameterized testing as supported in JUnit (and the alternative approaches).
Many developers consider parameterized testing a challenging and complicated process. The truth is that the limitations of JUnit make parameterized testing a chal- lenge, and developers suffer because of inertia and their resistance to changing their testing framework.
3.2.1 Existing approaches to multiple test-input parameters
The requirements for the nuclear-reactor monitor are clear, the software is already implemented, and you’re ready to test it. What’s the solution if you follow the status quo?
The recent versions of JUnit advertise support for parameterized tests. The official way of implementing a parameterized test with JUnit is shown in the following listing.
The listing assumes that –1 in evacuation minutes means that no evacuation is needed.
@RunWith(Parameterized.class) public class NuclearReactorTest {
private final int triggeredFireSensors;
private final List<Float> radiationDataReadings;
private final int pressure;
private final boolean expectedAlarmStatus;
private final boolean expectedShutdownCommand;
private final int expectedMinutesToEvacuate;
public NuclearReactorTest(int pressure, int triggeredFireSensors, List<Float> radiationDataReadings, boolean expectedAlarmStatus, boolean expectedShutdownCommand, int expectedMinutesToEvacuate) { this.triggeredFireSensors = triggeredFireSensors;
this.radiationDataReadings = radiationDataReadings;
this.pressure = pressure;
this.expectedAlarmStatus = expectedAlarmStatus;
this.expectedShutdownCommand = expectedShutdownCommand;
this.expectedMinutesToEvacuate = expectedMinutesToEvacuate;
7 A DSL is a programming language targeted at a specific problem as opposed to a general programming lan- guage like Java. See http://en.wikipedia.org/wiki/Domain-specific_language.
Listing 3.6 Testing the nuclear reactor scenarios with JUnit
Specialized runner needed for parameterized tests is created with @RunWith.
Inputs become class fields.
Outputs become class fields.
Special constructor with all inputs and outputs
73 Handling tests with multiple input sets
}
@Test
public void nuclearReactorScenario() { NuclearReactorMonitor nuclearReactorMonitor = new NuclearReactorMonitor();
nuclearReactorMonitor.feedFireSensorData(triggeredFireSensors);
nuclearReactorMonitor.feedRadiationSensorData(radiationDataReadings);
nuclearReactorMonitor.feedPressureInBar(pressure);
NuclearReactorStatus status = nuclearReactorMonitor.getCurrentStatus();
assertEquals("Expected no alarm", expectedAlarmStatus, status.isAlarmActive());
assertEquals("No notifications", expectedShutdownCommand, status.isShutDownNeeded());
assertEquals("No notifications", expectedMinutesToEvacuate, status.getEvacuationMinutes());
}
@Parameters
public static Collection<Object[]> data() { return Arrays.asList(new Object[][] {
{ 150, 0, new ArrayList<Float>(), false, false, -1 }, { 150, 1, new ArrayList<Float>(), true, false, -1 }, { 150, 3, new ArrayList<Float>(), true, true, -1 }, { 150, 0, Arrays.asList(110.4f, 0.3f, 0.0f), true, true, 1 }, { 150, 0, Arrays.asList(45.3f, 10.3f, 47.7f), false, false, -1 }, { 155, 0, Arrays.asList(0.0f, 0.0f, 0.0f), true, false, -1 },
{ 170, 0, Arrays.asList(0.0f, 0.0f, 0.0f), true, true, 3 },
{ 180, 0, Arrays.asList(110.4f, 0.3f, 0.0f), true, true, 1 }, { 500, 0, Arrays.asList(110.4f, 300f, 0.0f), true, true, 1 }, { 30, 0, Arrays.asList(110.4f, 1000f, 0.0f), true, true, 1 }, { 155, 4, Arrays.asList(0.0f, 0.0f, 0.0f), true, true, -1 },
{ 170, 1, Arrays.asList(45.3f, 10.3f, 47.7f), true, true, 3 }, });
} }
If you look at this code and feel it’s too verbose, you’re right! This listing is a true tes- tament to the limitations of JUnit. To accomplish parameterized testing, the following constraints specific to JUnit need to be satisfied:
■ The test class must be polluted with fields that represent inputs.
■ The test class must be polluted with fields that represent outputs.
Unit test that will use parameters
Source of test data Two-dimensional
array with test data
■ A special constructor is needed for all inputs and outputs.
■ Test data comes into a two-dimensional object array (which is converted to a list).
Notice also that because of these limitations, it’s impossible to add a second parame- terized test in the same class. JUnit is so strict that it forces you to have a single class for each test when multiple parameters are involved. If you have a Java class that needs more than one parameterized test and you use JUnit, you’re out of luck.8
The problems with JUnit parameterized tests are so well known that several inde- pendent efforts have emerged to improve this aspect of unit testing. At the time of writing, at least three external projects9 offer their own syntax on top of JUnit for a friendlier and less cluttered code.
Parameterized tests are also an area where TestNG (http://testng.org) has been advertised as a better replacement for JUnit. TestNG does away with all JUnit limita- tions and comes with extra annotations (@DataProvider) that truly decouple test data and test logic.
Despite these external efforts, Spock comes with an even better syntax for parame- ters (Groovy magic again!). In addition, having all these improved efforts external to JUnit further supports my argument that Spock is a “batteries-included” framework providing everything you need for testing in a single package.
3.2.2 Tabular data input with Spock
You’ve seen the hideous code of JUnit when multiple parameters are involved. You might have also seen some improvements with TestNG or extra JUnit add-ons. All these solutions attempt to capture the values of the parameters by using Java code or annotations.
Spock takes a step back and focuses directly on the original test scenarios. Return- ing to the nuclear-monitoring system, remember that what you want to test are the scenarios listed in table 3.1 (written in a human-readable format).
Spock allows you to do the unthinkable. You can directly embed this table as-is inside your Groovy code, as shown in the next listing. Again I assume that –1 in evacu- ation minutes means that no evacuation is needed.
class NuclearReactorSpec extends spock.lang.Specification{
def "Complete test of all nuclear scenarios"() { given: "a nuclear reactor and sensor data"
NuclearReactorMonitor nuclearReactorMonitor =new NuclearReactorMonitor()
8 There are ways to overcome this limitation, but I consider them hacks that make the situation even more com- plicated.
9 https://code.google.com/p/fuzztester/wiki/FuzzTester; https://github.com/Pragmatists/junitparams;
https://github.com/piotrturski/zohhak.
Listing 3.7 Testing the nuclear reactor scenarios with Spock
Human-readable test description
75 Handling tests with multiple input sets
when: "we examine the sensor data"
nuclearReactorMonitor.feedFireSensorData(fireSensors) nuclearReactorMonitor.feedRadiationSensorData(radiation) nuclearReactorMonitor.feedPressureInBar(pressure) NuclearReactorStatus status = nuclearReactorMonitor.getCurrentStatus() then: "we act according to safety requirements"
status.alarmActive == alarm status.shutDownNeeded == shutDown status.evacuationMinutes == evacuation where: "possible nuclear incidents are:"
pressure | fireSensors | radiation || alarm | shutDown | evacuation 150 | 0 | [] || false | false | -1 150 | 1 | [] || true | false | -1 150 | 3 | [] || true | true | -1 150 | 0| [110.4f ,0.3f, 0.0f] || true | true | 1 150 | 0| [45.3f ,10.3f, 47.7f]|| false | false | -1 155 | 0| [0.0f ,0.0f, 0.0f] || true | false | -1 170 | 0| [0.0f ,0.0f, 0.0f] || true | true | 3 180 | 0| [110.4f ,0.3f, 0.0f] || true | true | 1 500 | 0| [110.4f ,300f, 0.0f] || true | true | 1 30 | 0|[110.4f ,1000f, 0.0f] || true | true | 1 155 | 4| [0.0f ,0.0f, 0.0f] || true | true | -1 170 | 1| [45.3f ,10.3f, 47.7f]|| true | true | 3 }
}
Spock takes a different approach to parameters. Powered by Groovy capabilities, it offers a descriptive DSL for tabular data. The key point of this unit test is the where:
label (in addition to the usual given-then-when labels) that holds a definition of all inputs/outputs used in the other blocks.
In the where: block of this Spock test, I copied verbatim the scenarios of the nuclear-reactor monitor from the table. The || notation is used to split the inputs from outputs. Reading this table is possible even by nontechnical people. Your busi- ness analyst can look at this table and quickly locate missing scenarios.
Adding a new scenario is easy:
■ You can append a new line at the end of the table with a new scenario, and the test will pick the new scenario upon the next run.
■ The parameters are strictly contained inside the test method, unlike JUnit. The test class has no need for special constructors or fields. A single Spock class can hold an unlimited number of parameterized tests, each with its own tabular data.
The icing on the cake is the amount of code. The JUnit test has 82 lines of Java code, whereas the Spock test has 38 lines. In this example, I gained 50% code reduction by using Spock, and kept the same functionality as before (keeping my promise from chapter 1 that Spock tests will reduce the amount of test code in your application).
Usage of test inputs
Usage of test
outputs Source of
parameters Definition of
inputs and outputs
Tabular representation of all scenarios
Chapter 5 shows several other tricks for Spock parameterized tests, so feel free to jump there directly if your enterprise application is plagued by similar JUnit boiler- plate code.
We’ll close our Spock tour with its mocking/stubbing capabilities.