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' ?> 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 }) @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 .