In JUnit 4, I am looking to write a test suite that is made up of multiple flavors of the same test case, just with different initial conditions on each one. Here is an example:
import java.io.File;
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
import org.junit.runners.Suite.SuiteClasses;
@RunWith(Suite.class)
@SuiteClasses({MultiInputClientServerIntegrationTest.NormalInput.class,
MultiInputClientServerIntegrationTest.SimulationHashIssue.class})
public class MultiInputClientServerIntegrationTest {
@RunWith(Suite.class)
@SuiteClasses({TestClientServerIntegration.class})
public class NormalInput {}
@RunWith(Suite.class)
@SuiteClasses({TestClientServerIntegration.class})
public class SimulationHashIssue {
public SimulationHashIssue() {
TestClientServerIntegration.simulation = new File("test\\BEECHA01\\sim2.zip");
TestClientServerIntegration.inputFile = "files\\config.in";
}
}
}
As you can see, both inner classes have SuiteClasses of the TestClientServerIntegration.class
but the second one is changing some static variable values. I am finding that this constructor never gets called, so these statics never get changed.
My end goal is to run this TestClientServerIntegration.class
over and over with multiple types of input. If I can run a test suite this way, that would be ideal -- so hopefully it is possible. I'd like to do as little hacking of JUnit as possible, but what needs to get done will get done.
I solved it! The book JUnit in action helped a lot. Here is my code:
/**
* The Class MultiInputClientServerIntegrationTest.
*/
@RunWith(Suite.class)
@SuiteClasses({MultiInputClientServerIntegrationTest.NormalInput.class,
MultiInputClientServerIntegrationTest.BEECHA01SimulationHashIssue.class})
public class MultiInputClientServerIntegrationTest {
/**
* The Class NormalInput.
*/
@RunWith(Suite.class)
@SuiteClasses({TestClientServerIntegration.class})
public class NormalInput {}
/**
* The Class BEECHA01SimulationHashIssue.
*/
// @RunWith(Suite.class)
@RunWith(InterceptorRunner.class)
@SuiteClasses({TestClientServerIntegration.class})
@InterceptorClasses({BEECHA01SimulationHashIssueInterceptor.class})
public static class BEECHA01SimulationHashIssue extends TestClientServerIntegration { }
/**
* The Class BEECHA01SimulationHashIssueInterceptor.
*/
public static class BEECHA01SimulationHashIssueInterceptor implements Interceptor {
static File sim = new File("test\\BEECHA01\\6dof_Block20_FD2_2.zip");
static String in = "BEECHA01\\config.in";
/*
* (non-Javadoc)
*
* @see test.northgrum.globalhawk.simulation.Interceptor#interceptBefore()
*/
@Override
public void interceptBefore() {
if (!TestClientServerIntegration.simulation.equals(sim)
|| !TestClientServerIntegration.inputFile.equals(in)) {
TestClientServerIntegration.simulation = sim;
TestClientServerIntegration.inputFile = in;
System.out.println("Test set up with BEECHA01 Initial Parameters");
}
}
/*
* (non-Javadoc)
*
* @see test.northgrum.globalhawk.simulation.Interceptor#interceptAfter()
*/
@Override
public void interceptAfter() {}
}
}
Where the special Runners are:
/**
* This interface is used to declare the methods for every interceptor.
*
* @version $Id: Interceptor.java 201 2009-02-15 19:18:09Z paranoid12 $
*/
public interface Interceptor {
/**
* This method will be called before every test - we can implement our own logic in every
* implementation.
*/
public void interceptBefore();
/**
* This method will be called after every test - we can implement our own logic in every
* implementation.
*/
public void interceptAfter();
}
And,
/**
* A custom runner for JUnit4.5 in which we demonstrate the interceptor pattern.
*
* @version $Id: InterceptorRunner.java 201 2009-02-15 19:18:09Z paranoid12 $
*/
public class InterceptorRunner extends BlockJUnit4ClassRunner {
/**
* This is the InterceptorClasses annotation, which serves to hold our interceptor class
* implementations.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface InterceptorClasses {
/**
* Value.
*
* @return the classes to be run
*/
public Class<?>[] value();
}
/**
* This constructor is a must.
*
* @param clazz the test-case class
* @throws InitializationError the initialization error
*/
public InterceptorRunner(Class<?> clazz) throws InitializationError {
super(clazz);
}
/**
* Override the methodInvoker, so that when it is called we wrap the statement with our own.
*
* @param method the test method
* @param test the test-case
* @return the statement
*/
@Override
public Statement methodInvoker(FrameworkMethod method, Object test) {
InterceptorStatement statement = new InterceptorStatement(super.methodInvoker(method, test));
InterceptorClasses annotation = test.getClass().getAnnotation(InterceptorClasses.class);
Class<?>[] klasez = annotation.value();
try {
for (Class<?> klaz : klasez) {
statement.addInterceptor((Interceptor) klaz.newInstance());
}
} catch (IllegalAccessException ilex) {
ilex.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
return statement;
}
}
/**
* A statement for our custom runner.
*
* @version $Id: InterceptorStatement.java 201 2009-02-15 19:18:09Z paranoid12 $
*/
public class InterceptorStatement extends Statement {
/**
* A wrapping invoker that will procede the execution, once we execute our interceptors.
*/
private final Statement invoker;
/**
* A list of interceptors that will be executed before the other statements.
*/
private List<Interceptor> interceptors = new ArrayList<Interceptor>();
/**
* A constructor that takes another invoker to wrap our statement.
*
* @param invoker the invoker
*/
public InterceptorStatement(Statement invoker) {
this.invoker = invoker;
}
/**
* We override this method to call our interceptors, and then evaluate the wrapping invoker.
*
* @throws Throwable the throwable
*/
@Override
public void evaluate() throws Throwable {
for (Interceptor interceptor : interceptors) {
interceptor.interceptBefore();
}
invoker.evaluate();
for (Interceptor interceptor : interceptors) {
interceptor.interceptAfter();
}
}
/**
* Add another interceptor to the list of interceptors we have.
*
* @param interceptor we want to add
*/
public void addInterceptor(Interceptor interceptor) {
interceptors.add(interceptor);
}
}
What helped a lot was actually switching to JUnit 4.10 as it gave a more detailed error message. Anyway the main differences here is that I am having my "custom input" tests extend the actual test. Then I created an interceptor which gets overloads the @Before and @After methods and can alter parameters before each individual @Test.
In truth, I'd prefer something that just overloaded each @BeforeClass but beggars aren't choosers. This is good enough and does the job right. It works with Eclipse. Hopefully I'll run across a hook for @BeforeClass and work that instead.
What about an alternative solution?
1.Use template pattern to extract an abstract test class, and make the initial condition preparement an abstract method.
2.Each test case extends the template and override the implentation of initial condition preparement.
3.Group them all in a test suite.
Oh, keep it simple! Your test class can have
- a private method, which actually runs the test, and which has parameters corresponding to the various things you want to change from one test to the next;
- several public test methods, each of which just calls the private method, with different parameter values.