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 #1Normal test method #2An 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)@ContextConfigurationpublic 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'?> xsi:schemaLocation='http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd'> <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})@ContextConfigurationpublic 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)@ContextConfigurationpublic 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 hierarchy12: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 hierarchy12: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 hierarchy12: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 .