Статьи

Улучшение Spring Test Framework с настройкой beforeClass и afterClass

Как разрешить запуск методов экземпляра как поведение JUnit BeforeClass

JUnit позволяет вам настраивать методы на уровне класса до и после вызова всех методов тестирования. Однако специально они ограничивают это только статическими методами с использованием аннотаций @BeforeClass и @AfterClass . Например, эта простая демонстрация показывает типичную настройку Junit:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package deng.junitdemo;
 
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
 
public class DemoTest {
 
    @Test
    public void testOne() {
        System.out.println('Normal test method #1.');
    }
 
    @Test
    public void testTwo() {
        System.out.println('Normal test method #2.');
    }
 
    @BeforeClass
    public static void beforeClassSetup() {
        System.out.println('A static method setup before class.');
    }
 
    @AfterClass
    public static void afterClassSetup() {
        System.out.println('A static method setup after class.');
    }
}

И выше должен привести следующий вывод:

1
2
3
4
A static method setup before class.
Normal test method #1.
Normal test method #2.
A static method setup after class.

Такое использование подходит в большинстве случаев, но бывают случаи, когда вы хотите использовать нестатические методы для настройки теста. Позже я покажу вам более подробный пример использования, но сейчас давайте посмотрим, как мы можем сначала решить эту непослушную проблему с помощью JUnit. Мы можем решить эту проблему, сделав тест реализующий Listener, который обеспечивает обратные вызовы до и после, и нам нужно будет копаться в JUnit, чтобы обнаружить этот Listener для вызова наших методов. Это решение, которое я придумал:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package deng.junitdemo;
 
import org.junit.Test;
import org.junit.runner.RunWith;
 
@RunWith(InstanceTestClassRunner.class)
public class Demo2Test implements InstanceTestClassListener {
 
    @Test
    public void testOne() {
        System.out.println('Normal test method #1');
    }
 
    @Test
    public void testTwo() {
        System.out.println('Normal test method #2');
    }
 
    @Override
    public void beforeClassSetup() {
        System.out.println('An instance method setup before class.');
    }
 
    @Override
    public void afterClassSetup() {
        System.out.println('An instance method setup after class.');
    }
}

Как указано выше, наш слушатель представляет собой простой контракт:

1
2
3
4
5
6
package deng.junitdemo;
 
public interface InstanceTestClassListener {
    void beforeClassSetup();
    void afterClassSetup();
}

Наша следующая задача — предоставить реализацию бегуна JUnit, которая будет запускать методы установки.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package deng.junitdemo;
 
import org.junit.runner.notification.RunNotifier;
import org.junit.runners.BlockJUnit4ClassRunner;
import org.junit.runners.model.InitializationError;
 
public class InstanceTestClassRunner extends BlockJUnit4ClassRunner {
 
    private InstanceTestClassListener InstanceSetupListener;
 
    public InstanceTestClassRunner(Class<?> klass) throws InitializationError {
        super(klass);
    }
 
    @Override
    protected Object createTest() throws Exception {
        Object test = super.createTest();
        // Note that JUnit4 will call this createTest() multiple times for each
        // test method, so we need to ensure to call 'beforeClassSetup' only once.
        if (test instanceof InstanceTestClassListener && InstanceSetupListener == null) {
            InstanceSetupListener = (InstanceTestClassListener) test;
            InstanceSetupListener.beforeClassSetup();
        }
        return test;
    }
 
    @Override
    public void run(RunNotifier notifier) {
        super.run(notifier);
        if (InstanceSetupListener != null)
            InstanceSetupListener.afterClassSetup();
    }
}

Сейчас мы находимся в бизнесе. Если мы запустим тест выше, он должен дать нам аналогичный результат, но на этот раз мы используем методы экземпляра!

1
2
3
4
An instance method setup before class.
Normal test method #1
Normal test method #2
An instance method setup after class.


Конкретный вариант использования: работа с Spring Test Framework

Теперь позвольте мне показать вам реальный пример использования выше. Если вы используете Spring Test Framework, вы обычно настраиваете тест, подобный этому, чтобы в качестве экземпляра элемента можно было вставить тестовое устройство.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package deng.junitdemo.spring;
 
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
 
import java.util.List;
 
import javax.annotation.Resource;
 
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
 
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
public class SpringDemoTest {
 
    @Resource(name='myList')
    private List<String> myList;
 
    @Test
    public void testMyListInjection() {
        assertThat(myList.size(), is(2));
    }
}

Вам также понадобится Spring xml под тем же пакетом для запуска выше:

01
02
03
04
05
06
07
08
09
10
11
12
13
<?xml version='1.0' encoding='UTF-8'?>
     <bean id='myList' class='java.util.ArrayList'>
        <constructor-arg>
            <list>
                <value>one</value>
                <value>two</value>
            </list>
        </constructor-arg>
     </bean>
</beans>

Обратите очень пристальное внимание на экземпляр экземпляра List<String> myList . При запуске теста JUnit это поле будет вставлено Spring и может использоваться в любом методе тестирования. Однако, если вам когда-нибудь понадобится выполнить однократную настройку некоторого кода и получить ссылку на поле, внедренное в Spring, то вам не повезло. Это потому, что JUnit @BeforeClass заставит ваш метод быть статическим; и если вы сделаете ваше поле статичным, Spring Injection не будет работать в вашем тесте!

Теперь, если вы частый пользователь Spring, вы должны знать, что Spring Test Framework уже предоставил вам способ справиться с этим типом сценария использования. Вот способ настройки уровня класса в стиле Spring:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package deng.junitdemo.spring;
 
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
 
import java.util.List;
 
import javax.annotation.Resource;
 
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestContext;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.support.AbstractTestExecutionListener;
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
 
@RunWith(SpringJUnit4ClassRunner.class)
@TestExecutionListeners(listeners = {
        DependencyInjectionTestExecutionListener.class,
        SpringDemo2Test.class})
@ContextConfiguration
public class SpringDemo2Test extends AbstractTestExecutionListener {
 
    @Resource(name='myList')
    private List<String> myList;
 
    @Test
    public void testMyListInjection() {
        assertThat(myList.size(), is(2));
    }
 
    @Override
    public void afterTestClass(TestContext testContext) {
        List<?> list = testContext.getApplicationContext().getBean('myList', List.class);
        assertThat((String)list.get(0), is('one'));
    }
 
    @Override
    public void beforeTestClass(TestContext testContext) {
        List<?> list = testContext.getApplicationContext().getBean('myList', List.class);
        assertThat((String)list.get(1), is('two'));
    }
}

Как вы можете видеть, Spring предлагает аннотацию @TestExecutionListeners чтобы позволить вам написать любого Слушателя, и в нем у вас будет ссылка на TestContext который имеет ApplicationContext для вас, чтобы перейти к TestContext ссылке на поле. Это работает, но я нахожу это не очень элегантным. Это заставляет вас искать бин, в то время как ваше введенное поле уже доступно как поле. Но вы не можете использовать его, если не TestContext через параметр TestContext .

Теперь, если вы смешаете решение, которое мы предоставили в начале, мы увидим более приятную настройку теста. Давай увидим это:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package deng.junitdemo.spring;
 
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
 
import java.util.List;
 
import javax.annotation.Resource;
 
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
 
import deng.junitdemo.InstanceTestClassListener;
 
@RunWith(SpringInstanceTestClassRunner.class)
@ContextConfiguration
public class SpringDemo3Test implements InstanceTestClassListener {
 
    @Resource(name='myList')
    private List<String> myList;
 
    @Test
    public void testMyListInjection() {
        assertThat(myList.size(), is(2));
    }
 
    @Override
    public void beforeClassSetup() {
        assertThat((String)myList.get(0), is('one'));
    }
 
    @Override
    public void afterClassSetup() {
        assertThat((String)myList.get(1), is('two'));
    }
}

Теперь JUnit позволяет вам использовать только один Runner , поэтому мы должны расширить версию Spring, чтобы вставить то, что мы делали раньше.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package deng.junitdemo.spring;
 
import org.junit.runner.notification.RunNotifier;
import org.junit.runners.model.InitializationError;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
 
import deng.junitdemo.InstanceTestClassListener;
 
public class SpringInstanceTestClassRunner extends SpringJUnit4ClassRunner {
 
    private InstanceTestClassListener InstanceSetupListener;
 
    public SpringInstanceTestClassRunner(Class<?> clazz) throws InitializationError {
        super(clazz);
    }
 
    @Override
    protected Object createTest() throws Exception {
        Object test = super.createTest();
        // Note that JUnit4 will call this createTest() multiple times for each
        // test method, so we need to ensure to call 'beforeClassSetup' only once.
        if (test instanceof InstanceTestClassListener && InstanceSetupListener == null) {
            InstanceSetupListener = (InstanceTestClassListener) test;
            InstanceSetupListener.beforeClassSetup();
        }
        return test;
    }
 
    @Override
    public void run(RunNotifier notifier) {
        super.run(notifier);
        if (InstanceSetupListener != null)
            InstanceSetupListener.afterClassSetup();
    }
}

Это должно делать свое дело. Запуск теста даст использование этого вывода:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
12:58:48 main INFO  org.springframework.test.context.support.AbstractContextLoader:139 | Detected default resource location 'classpath:/deng/junitdemo/spring/SpringDemo3Test-context.xml' for test class [deng.junitdemo.spring.SpringDemo3Test].
12:58:48 main INFO  org.springframework.test.context.support.DelegatingSmartContextLoader:148 | GenericXmlContextLoader detected default locations for context configuration [ContextConfigurationAttributes@74b23210 declaringClass = 'deng.junitdemo.spring.SpringDemo3Test', locations = '{classpath:/deng/junitdemo/spring/SpringDemo3Test-context.xml}', classes = '{}', inheritLocations = true, contextLoaderClass = 'org.springframework.test.context.ContextLoader'].
12:58:48 main INFO  org.springframework.test.context.support.AnnotationConfigContextLoader:150 | Could not detect default configuration classes for test class [deng.junitdemo.spring.SpringDemo3Test]: SpringDemo3Test does not declare any static, non-private, non-final, inner classes annotated with @Configuration.
12:58:48 main INFO  org.springframework.test.context.TestContextManager:185 | @TestExecutionListeners is not present for class [class deng.junitdemo.spring.SpringDemo3Test]: using defaults.
12:58:48 main INFO  org.springframework.beans.factory.xml.XmlBeanDefinitionReader:315 | Loading XML bean definitions from class path resource [deng/junitdemo/spring/SpringDemo3Test-context.xml]
12:58:48 main INFO  org.springframework.context.support.GenericApplicationContext:500 | Refreshing org.springframework.context.support.GenericApplicationContext@44c9d92c: startup date [Sat Sep 29 12:58:48 EDT 2012]; root of context hierarchy
12:58:49 main INFO  org.springframework.beans.factory.support.DefaultListableBeanFactory:581
| Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@73c6641: defining beans
 
[myList,org.springframework.context.annotation.
internalConfigurationAnnotationProcessor,org.
springframework.context.annotation.internalAutowiredAnnotationProcessor,org
.springframework.context.annotation.internalRequiredAnnotationProcessor,org.
springframework.context.annotation.internalCommonAnnotationProcessor,org.
springframework.context.annotation.
ConfigurationClassPostProcessor$ImportAwareBeanPostProcessor#0]; root of factory hierarchy
12:58:49 Thread-1 INFO  org.springframework.context.support.GenericApplicationContext:1025 | Closing org.springframework.context.support.GenericApplicationContext@44c9d92c: startup date [Sat Sep 29 12:58:48 EDT 2012]; root of context hierarchy
12:58:49 Thread-1 INFO  org.springframework.beans.factory.support.
DefaultListableBeanFactory:433
| Destroying singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@
73c6641: defining beans [myList,org.springframework.context.annotation.
internalConfigurationAnnotationProcessor,org.springframework.
context.annotation.internalAutowiredAnnotationProcessor,org.springframework.
context.annotation.internalRequiredAnnotationProcessor,org.springframework.
context.annotation.internalCommonAnnotationProcessor,org.springframework.
context.annotation.ConfigurationClassPostProcessor$ImportAwareBeanPostProcessor#0]; root of factory hierarchy

Очевидно, что вывод не показывает ничего интересного здесь, но тест должен выполняться со всеми утвержденными утверждениями. Дело в том, что теперь у нас есть более элегантный способ вызывать настройки до и после теста, которые находятся на уровне класса, и они могут быть методами экземпляра, позволяющими внедрение Spring.

Загрузите демонстрационный код

Вы можете получить приведенный выше демонстрационный код в рабочем проекте Maven из моей песочницы.

Ссылка: Улучшение Spring Test Framework с установкой beforeClass и afterClass от нашего партнера JCG Земьяна Дена в блоге A Programmer’s Journal .