poniedziałek, 7 maja 2012

DependencyInjectionJunitRulesTestExecutionListener

Ostatnio strasznie spodobały mi się rozszerzenia do @Rule do junit. Pomimo tego że widzę pewne problemy ze stabilnością całego mechanizmu (jak np, zamiana kolejności uruchamiania @Rule a metod @Before i @After) i kilku braków ( m.in. nie działają @Rule zdefiniowane na poziomie suite - dla klas z @RunWith (? extend Suite) ) )  to rozwiązanie jest na prawdę super.
Dzięki wykorzystaniu @Rule możemy bardzo ładnie enkapsulować logikę związaną z wykonaniem testów ,np. współbieżne wykonanie metody testowej , modyfikowanie/zawieszanie metod testowych. Mechanizm jest na tyle uniwersalny, że w przyszłości annotacje @Before/@After maja zostać uznane za deprecated na rzecz wykorzystania @Rule. Aktualnie junit w wersji 4.10 wymaga aby anotacja @Rule była umieszczona na publicznym nie statycznym property typu TestRule lub MethodRule (deprecated). Co ciekawe wygląda na to, że @Rule działają bardzo dobrze z Spring TestContext Framework (przynajmniej dla junit-4.10 oraz SpringJUnit4ClassRunner z wersji springa 3.1.1).  Integracja może wyglądać w dwojaki sposób:
  • DI dla implementacji interfejsu TestRule
  • TestRule zdefiniowane bezpośrednio w spring
W pierwszym przypadku stworzyłem bardzo prosty listener który  injectuje beany do @Rule  i @ClassRule:
public class DependencyInjectionJunitRulesTestExecutionListener extends
        AbstractTestExecutionListener {
   @Override
    public void prepareTestInstance(final TestContext testContext)
            throws Exception {
        Object testInstance = testContext.getTestInstance();
        List<TestRule> ruleFields = getRuleFields(testInstance);
        doInjection(testContext, ruleFields);
    }

    private void doInjection(final TestContext testContext,
            List<TestRule> ruleFields) {
        AutowireCapableBeanFactory beanFactory = testContext
                .getApplicationContext().getAutowireCapableBeanFactory();
        for (TestRule testRule : ruleFields) {
            beanFactory.autowireBeanProperties(testRule,
                    AutowireCapableBeanFactory.AUTOWIRE_NO, false);
            beanFactory.initializeBean(testRule, testContext.getTestClass()
                    .getName());
        }
    }

    private List<TestRule> getRuleFields(Object testInstance) {
        TestClass testClass = new TestClass(testInstance.getClass());
        return testClass.getAnnotatedFieldValues(testInstance, Rule.class,
                TestRule.class);
    }

    @Override
    public void beforeTestClass(TestContext testContext) throws Exception {
        List<TestRule> classRuleFields = getClassRuleFields(testContext);
        doInjection(testContext, classRuleFields);
    }

    private List<TestRule> getClassRuleFields(TestContext testContext) {
        TestClass testClass = new TestClass(testContext.getTestClass());
        return testClass.getAnnotatedFieldValues(null, ClassRule.class,
                TestRule.class);
    }
   


}
Implementacja bazuję na dostępnym out-of-box DependencyInjectionTestExecutionListener oraz TestClass. Wszystko wygląda nieźle, ale niestety nie do końca działa :(. Brakuję na pewno reinjectowania beanów dla @ClassRule  w przypadku gdy nasza metoda/klasa używa @DirtiesContext. Implementacja nie powinna być trudna gdyż DirtiesContextTestExecutionListener ustawia odpowiednią flagę na TestContext (niestety ta flaga jest usuwana przez DependencyInjectionTestExecutionListener, czyli trzeba albo nasz listener ustawić przed DependencyInjectionTestExecutionListener albo rozszerzyć ten ostatni).
Większym problem jest jednak to, że aktualnie dla Spring 3.1.1.RELEASE odpowiednie metody klasy TestExecutionListener, a dokładnie metoda beforeTestClass jest wywoływana po @ClassRule. "Winna" temu jest metoda classBlock w klasie ParentRunner, po której to dziedziczy SpringJUnit4ClassRunner. Zgodnie z tym co można znaleźć w javadoc @ClassRule opakowują uruchomienie @BeforeClass/@AfterClass, a te z kolei SpringJUnit4ClassRunner opakowuje w wywołanie TestExecutionListener#beforeTestClass. Reasumując: najpierw zostaną uruchomiene @ClassRule, następnie TestExecutionListener#beforeTestClass a na końcu metody @BeforeClass. Metody definiujące zadaną kolejność oraz wywołania są protected, czyli wygląda na to, że zmiana jest jak najbardziej do zrobienia. W innym przypadku, tzn gdy nie potrzebujemy injectowania dla @ClassRule z przedstawionego powyżej kodu można spokojnie wyrzucić metodę beforeTestClass.
Jeśli chodzi o drugie podejście to sprawa wygląda całkiem podobnie. Tzn. w fazie TestExecutionListener#prepareTestInstance jest wykonywane DI, które to może injectować do property z anotacją @Rule. Jeśli chodzi o @ClassRule to DI dla pól statycznych nie działa by design. Są pewne obejścia, ale chyba bez jakiegoś większego plumbingu się nie obejdzie.
EDIT: aktualnie pojawiła się potrzeba stworzenia odpowiedniego test fixture, który to byłby dostępny dla kilku metod testowych. Można to łatwo zaimplementować jako TestRule albo metodę @Before, jednak ze względu na to, że tworzenie tego fixure może być czasochłonne lepiej zrobić to tylko raz. W szczególności chodzi o stworzenie i "ustawienie" stanu odpowiedniego bytu w systemie przy wykorzystaniu Selenium (Webdriver) aby testy w danej klasie mogły bazować na tak przygotowanym bycie. W przypadku JUnit najsensowniejsze wydaje się wykorzystanie @BeforeClass. W moim przypadku instancja WebDrivera jest injectowana przez Spring TestContext Framework, a skoro injectowanie do pól statycznych (tak aby były do użycia przez @BeforeClass) nie działa out-of box zdecydowałem się na to aby tworzyć test fixture za pomocą ClassRule - DI dla ClassRule zostało zaimplementowane w DependencyInjectionJunitRulesTestExecutionListener#beforeTestClass. Problemem jest tylko to, że w aktualnej implementacji Spring TestContext Framework bazuje na Junit 4.5, czyli SpringJUnit4ClassRunner uruchamia TestExecutionListener#beforeTestClass przed @BeforeClass, ale po odpaleniu ClassRule. Zmiana tej kolejności okazała się stosunkowo prosta, ale potrzebowałem stworzyć nowy Runner:
public class RunSpringAroundClassRulesJUnit4ClassRunner extends
        SpringJUnit4ClassRunner {

    public static class AroundTestClassCallbacks extends Statement {
        private final Statement statement;

        public AroundTestClassCallbacks(Statement next,
                TestContextManager testContextManager) {
            this.statement = new RunAfterTestClassCallbacks(
                    new RunBeforeTestClassCallbacks(next, testContextManager),
                    testContextManager);
        }
        @Override
        public void evaluate() throws Throwable {
            statement.evaluate();
        }
    }

    public RunSpringAroundClassRulesJUnit4ClassRunner(Class<?> clazz)
            throws InitializationError {
        super(clazz);
    }

    @Override
    protected Statement classBlock(final RunNotifier notifier) {
        Statement statement = childrenInvoker(notifier);
        statement = withBeforeClasses(statement);
        statement = withAfterClasses(statement);
        statement = withClassRules(statement);
        statement = withAroundTestClass(statement);
        return statement;
    }

    private Statement withAroundTestClass(Statement statement) {
        return new AroundTestClassCallbacks(statement, getTestContextManager());
    }

    @Override
    protected Statement withBeforeClasses(Statement statement) {
        List<FrameworkMethod< befores = getTestClass().getAnnotatedMethods(
                BeforeClass.class);
        return befores.isEmpty() ? statement : new RunBefores(statement,
                befores, null);
    }

    @Override
    protected Statement withAfterClasses(Statement statement) {
        List<FrameworkMethod> afters = getTestClass().getAnnotatedMethods(
                AfterClass.class);
        return afters.isEmpty() ? statement : new RunAfters(statement, afters,
                null);
    }

    private Statement withClassRules(Statement statement) {
        List<TestRule> classRules = classRules();
        return classRules.isEmpty() ? statement : new RunRules(statement,
                classRules, getDescription());
    }
}