To Clone or not to Clone
If two or more JExample tests depend on the same return value, something must be done to prevent side-effects. The default policy is to clone the cached return value before injection. More policies are available through the @Injection annotation.
Let’s consider an example with one producer and two consumers:
@RunWith(JExample.class)
@Injection(InjectionPolicy.NONE) // disables cloning, don't do this at home!
public class StackTest {
@Test
public Stack emptyStack() {
return new Stack();
}
@Given("#emptyStack")
public void shouldPushFoo(Stack stack) {
stack.push("foo");
assertEquals(1, stack.size());
}
@Given("#emptyStack")
public void shouldPushBar(Stack stack) {
stack.push("bar");
assertEquals(1, stack.size());
}
}
If we run this test class either of the consumers #shouldPushFoo or #shouldPushBar (depending on the order of execution on your machine) will fail with “expected:<1> but was <2>” as error message.
Why?
The test framework runs the producer #emptyStack once and caches its return value. When the first consumer is about to run, it is called with the cached return value as parameter. The consumer then modifies the provided value in order to execute its test. However, if no special measures are taken, this modification will be visible to the second consumer, which is later called with the same cached return value!
JExample offers three strategies to avoid tainted injection values:
-
InjectionPolicy.CLONEuses the#clonemethod to clone all provided return values. If cloning fails for a value (typically because the value does not implementCloneable), its provider is rerun to obtain an untainted value. -
InjectionPolicy.DEEPCOPYuses internal reflection to create a field-by-field copy of all provided injection values and all objects reachable through these values (ie deep copy). This strategy does not call the#clonemethod and thus even copies values that don’t implementCloneable. Use with caution: this strategy might cause the VM to die without further notice, since some objects are just not ment to be copied in that radical way. -
InjectionPolicy.RERUNreruns all providers whenever injection values are required. This strategy is most stable, however you lose the benefit of faster test execution.
By default, JExample uses the CLONE policy.
Lookup of injection policies is resolved as follows: first JExample looks at the consumer method, then at the current class and all its superclasses, then at the current package and the package of all superclasses, and eventually it defaults to InjectionPolicy.DEFAULT (which, by default, defaults to InjectionPolicy.CLONE).
Injection policies are available since JExample revision 374, released in August 2009. You can obtain the latest version either as plain JAR file or as Eclipse library (update site).
August 26th, 2009 at 20:44
“InjectionPolicy.RERUN reruns all providers whenever injection values are required. This strategy is most stable, however you lose the benefit of faster test execution.” At least the “stack-creating”-provider is only run two times in the example, instead of three times, as it would be the case if creating the stack would be a real test and not a mere setup.
So do you really “lose the benefit of faster test execution.”? A bit yes, but can you estimate the average number of reuses of provider test cases? Say if test cases would be reused only once on average, the Rerun-policy would make a decent default.
In general it is good to have those three options of course you came up with.
Keep up that great work including this blog
Cheers
Markus
August 27th, 2009 at 10:31
Good point, I like to idea of a dynamic default. This could even be done per node: if there is only one consumer RERUN should be the default.
In general, if there are n consumer, only n-1 results need to be cloned thus we could omit the clone for the last consumer. Alas, if a grandchild-consumer triggers a rerun of a child-consumer, this child consumes its input twice or more. I am still trying to figure out the best strategy to omit unnecessary clones. We are also working on a solution to dispose cached return values after their last use to reduce the memory footprint of running large test suites.