How can I use the Parameterized JUnit test runner

2019-01-23 13:50发布

问题:

I'm using Spring to inject the path to a directory into my unit tests. Inside this directory are a number of files that should be used to generate test data for parameterized test cases using the Parameterized test runner. Unfortunately, the test runner requires that the method that provides the parameters be static. This doesn't work for my situation because the directory can only be injected into a non-static field. Any ideas how I can get around this?

回答1:

I assume you are using JUnit 4.X since you mentioned the Parameterized test runner. This implies you aren't using @RunWith(SpringJUnit4ClassRunner). Not a problem, just listing my assumptions.

The following uses Spring to get the test files directory from the XML file. It doesn't inject it, but the data is still available to your test. And in a static method no less.

The only disadvantage I see is that it may mean your Spring config is getting parsed/configured multiple times. You could load just a smaller file with test specific info if need be.

@RunWith(Parameterized.class)
public class MyTest {
    @Parameters
    public static Collection<Object[]> data() {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("/jeanne/jeanne.xml");
        String dir = ctx.getBean("testFilesDirectory", String.class);

            // write java code to link files in test directory to the array
        return Arrays.asList(new Object[][] { { 1 } });
    }
// rest of test class
}


回答2:

You can use a TestContextManager from Spring. In this example, I'm using Theories instead of Parameterized.

@RunWith(Theories.class)
@ContextConfiguration(locations = "classpath:/spring-context.xml")
public class SeleniumCase {
  @DataPoints
  public static WebDriver[] drivers() {
    return new WebDriver[] { firefoxDriver, internetExplorerDriver };
  }

  private TestContextManager testContextManager;

  @Autowired
  SomethingDao dao;

  private static FirefoxDriver firefoxDriver = new FirefoxDriver();
  private static InternetExplorerDriver internetExplorerDriver = new InternetExplorerDriver();

  @AfterClass
  public static void tearDown() {
    firefoxDriver.close();
    internetExplorerDriver.close();
  }

  @Before
  public void setUpStringContext() throws Exception {
    testContextManager = new TestContextManager(getClass());
    testContextManager.prepareTestInstance(this);
  }

  @Theory
  public void testWork(WebDriver driver) {
    assertNotNull(driver);
    assertNotNull(dao);
  }
}

I found this solution here : How to do Parameterized/Theories tests with Spring



回答3:

For someone reading this late 2015 or later, Spring 4.2 has, in addition to SpringJUnit4ClassRunner added SpringClassRule and SpringMethodRule which leverage the support for Spring TestContext Framework.

This means first class support for any Runner like MockitoJUnitRunner or Parameterized:

@RunWith(Parameterized.class)
public class FibonacciTest {
    @ClassRule public static final SpringClassRule SCR = new SpringClassRule();
    @Rule public final SpringMethodRule springMethodRule = new SpringMethodRule();

    long input;
    long output;

    public FibonacciTest(long input, long output) { this.input = input; ...} 

    @Test
    public void testFibonacci() {
        Assert.assertEquals(output, fibonacci(input));
    }

    public List<Long[]> params() {
        return Arrays.asList(new Long[][] { {0, 0}, {1, 1} });
    }
}


回答4:

It's enough to annotate test class with @RunWith(Parameterized.class) and @ContextConfiguration, use @Autowired for dependency injection and use TestContextManager in constructor for initialization, e.g.:

@RunWith(Parameterized.class)
@ContextConfiguration(classes = TestConfig.class)
public class MyTest {

    @Autowired
    private DataSource dataSource;

    private final int param;

    @Parameterized.Parameters
    public static List<Object[]> params() {
        return Arrays.asList(new Object[][]{
            {1},
            {2},
        });
    }

    public MyTest(int p) {
        this.param = p;
        new TestContextManager(getClass()).prepareTestInstance(this);
    }

    @Test
    public void testSomething() {
       …
    }
}


回答5:

I use the following solution with the Parameterized.class without any problem: http://bmocanu.ro/coding/320/combining-junit-theoriesparameterized-tests-with-spring/

@ContextConfiguration(value = "classpath:test-context.xml")
public abstract class AbstractJunitTest extends AbstractJUnit4SpringContextTests {
    private static TestContextManager testContextManager = null;
    private static DAOFactory daoFactory = null;

    @Before
    public void initApplicationContext() throws Exception {
        if (testContextManager == null) {
            testContextManager = new TestContextManager(getClass());
            testContextManager.prepareTestInstance(this);
            daoFactory = (DAOFactory)applicationContext.getBean("daoFactory");
        }
    }

    protected DAOFactory getDaoFactory() throws Exception {
        return daoFactory;
    }
}



@RunWith(Parameterized.class)
public class SomeTestClass extends AbstractJunitTest {
     ...
}


回答6:

Here is a first solution without JUnit 4.12 parameterized factory, below an improved solution with it.

Static context without transactional support

Let Spring do all configuration parsing and autowiring with TestContextManager class.

The trick is to use a fake test instance to get autowired fields and pass them to the parameterized test which will effectively run.

But keep in mind prepareTestInstance() do the autowiring but doesn't manage test transaction and other nice stuffs handled by beforeTestMethod() and afterTestMethod().

@RunWith(Parameterized.class)
@ContextConfiguration(locations = {"/test-context.xml", "/mvc-context.xml"})
@WebAppConfiguration
@ActiveProfiles("test-profile")
public class MyTest {

  @Parameters
  public static Collection<Object[]> params() throws Exception {
    final MyTest fakeInstance = new MyTest();
    final TestContextManager contextManager = new TestContextManager(MyTest.class);
    contextManager.prepareTestInstance(fakeInstance);
    final WebApplicationContext context = fakeInstance.context;

    // Do what you need with Spring context, you can even access web resources
    final Resource[] files = context.getResources("path/files");
    final List<Object[]> params = new ArrayList<>();
    for (Resource file : files) {
      params.add(new Object[] {file, context});
    }
    return params;
  }

  @Parameter
  public Resource file;
  @Autowired
  @Parameter(1)
  public WebApplicationContext context;
}

However a drawback appear if you have a lot of autowired fields because you have to manually pass them to the array parameters.

Parameterized factory with full Spring support

JUnit 4.12 introduce ParametersRunnerFactory which allow to combine parameterized test and Spring injection.

public class SpringParametersRunnerFactory implements ParametersRunnerFactory {
@Override
  public Runner createRunnerForTestWithParameters(TestWithParameters test) throws InitializationError {
    final BlockJUnit4ClassRunnerWithParameters runnerWithParameters = new BlockJUnit4ClassRunnerWithParameters(test);
    return new SpringJUnit4ClassRunner(test.getTestClass().getJavaClass()) {
      @Override
      protected Object createTest() throws Exception {
        final Object testInstance = runnerWithParameters.createTest();
        getTestContextManager().prepareTestInstance(testInstance);
        return testInstance;
      }
    };
  }
}

The factory can be added to previous test class to give full Spring support like test transaction, reinit dirty context and servlet test. And of course there no more need to pass autowired fields from fake test instance to parameterized test.

@UseParametersRunnerFactory(SpringParametersRunnerFactory.class)
@RunWith(Parameterized.class)
@ContextConfiguration(locations = {"/test-context.xml", "/mvc-context.xml"})
@WebAppConfiguration
@Transactional
@TransactionConfiguration
public class MyTransactionalTest {

  @Parameters
  public static Collection<Object[]> params() throws Exception {
    final MyTransactionalTest fakeInstance = new MyTransactionalTest();
    final TestContextManager contextManager = new TestContextManager(MyTransactionalTest.class);
    contextManager.prepareTestInstance(fakeInstance);
    final WebApplicationContext context = fakeInstance.context;

    // Do what you need with Spring context, you can even access web resources
    final Resource[] files = context.getResources("path/files");
    final List<Object[]> params = new ArrayList<>();
    for (Resource file : files) {
      params.add(new Object[] {file});
    }
    return params;
  }

  @Parameter
  public Resource file;

  @Autowired
  private WebApplicationContext context;
}


回答7:

Remember that Spring inject using @Autowired, but also with setter. So instead of using @Autowired, use the setter:

private static String directory;

public void setDirectory(String directory) {
    this.directory = directory;
}

public static String getDirectory() {
     return directory;
}