Date post: | 10-May-2015 |
Category: |
Technology |
Upload: | sam-brannen |
View: | 11,069 times |
Download: | 5 times |
Spring 3.1 and MVCTesting Support
Sam BrannenSwiftmind
Rossen StoyanchevSpringSource, VMware
SpringOne 2GXOctober 28, 2011
Sam BrannenSenior Software Consultant @ Swiftmind
Java developer with 13+ years' experience
Spring Framework Core Developer
Lead author of Spring in a Nutshell
Previous SpringSource dm Server™ developer
Presenter on Spring, Java, OSGi, and testing
Rossen StoyanchevStaff Engineer SpringSource, VMware
Spring MVC, Spring Web Flow committer
Teach and consult, Spring Projects
Spring Web course author
NYC area
Goals of thePresentation
Gain an overview of testing featuresin Spring 3.1
Learn about the new Spring MVCTest Support project
AgendaSpring TestContext Framework
Testing with @Configuration Classes
Testing with Environment Profiles
Spring MVC Test Support
Q&A
Show of Hands...JUnit / TestNG
Spring 2.5 / 3.0 / 3.1
Integration testing with Spring
Spring TestContext Framework
Spring MVC
Spring MVC Test Support
Spring TestContextFramework
Introduced in Spring 2.5
Revised in Spring 3.1
Unit and integration testing
Annotation-driven
Convention over Configuration
JUnit & TestNG
Spring & Unit TestingPOJO-based programming model
Program to interfaces
IoC / Dependency Injection
Out-of-container testability
Testing mocks/stubs for various APIs: Servlet,Portlet, JNDI
General purpose testing utilitiesReflectionTestUtilsModelAndViewAssert
Spring & Integration TestingApplicationContext management & caching
Dependency injection of test instances
Transactional test managementwith default rollback semantics
SimpleJdbcTestUtils
JUnit 3.8 support classes are deprecated as ofSpring 3.0/3.1
Spring Test AnnotationsApplication Contexts
@ContextConfiguration, @DirtiesContext
@TestExecutionListeners
Dependency Injection@Autowired, @Qualifier, @Inject, …
Transactions@Transactional, @TransactionConfiguration, @Rollback,@BeforeTransaction, @AfterTransaction
Spring JUnit AnnotationsTesting Profiles
groups, not bean definitionprofiles@IfProfileValue,@ProfileValueSourceConfiguration
JUnit extensions@Timed, @Repeat
Using the TestContextFramework
Use the SpringJUnit4ClassRunner forJUnit 4.5+
Instrument test class withTestContextManager for TestNG
Extend one of the base classesAbstract(Transactional)[JUnit4|TestNG]SpringContextTests
Example: @POJO Test Class
public class OrderServiceTests {
@Test public void testOrderService() { … }}
Example: @POJO Test Class@RunWith(SpringJUnit4ClassRunner.class)
public class OrderServiceTests {
@Test public void testOrderService() { … }}
Example: @POJO Test Class@RunWith(SpringJUnit4ClassRunner.class)@ContextConfiguration
public class OrderServiceTests {
@Test public void testOrderService() { … }}
Example: @POJO Test Class@RunWith(SpringJUnit4ClassRunner.class)@ContextConfiguration // defaults to// OrderServiceTests-context.xml in same packagepublic class OrderServiceTests {
@Test public void testOrderService() { … }}
Example: @POJO Test Class@RunWith(SpringJUnit4ClassRunner.class)@ContextConfiguration // defaults to// OrderServiceTests-context.xml in same packagepublic class OrderServiceTests { @Autowired private OrderService orderService;
@Test public void testOrderService() { … }}
Example: @POJO Test Class@RunWith(SpringJUnit4ClassRunner.class)@ContextConfiguration // defaults to// OrderServiceTests-context.xml in same package@Transactionalpublic class OrderServiceTests { @Autowired private OrderService orderService;
@Test public void testOrderService() { … }}
Example: @POJO Test Class@RunWith(SpringJUnit4ClassRunner.class)@ContextConfiguration // defaults to// OrderServiceTests-context.xml in same package@Transactionalpublic class OrderServiceTests { @Autowired private OrderService orderService; @BeforeTransaction public void verifyInitialDatabaseState() { … }
@Test public void testOrderService() { … }}
Example: @POJO Test Class@RunWith(SpringJUnit4ClassRunner.class)@ContextConfiguration // defaults to// OrderServiceTests-context.xml in same package@Transactionalpublic class OrderServiceTests { @Autowired private OrderService orderService; @BeforeTransaction public void verifyInitialDatabaseState() { … } @Before public void setUpTestDataWithinTransaction() { … } @Test public void testOrderService() { … }}
Core Components
TestContextTracks context for current test
Delegates to a ContextLoader
Caches ApplicationContext
TestContextManagerManages the TestContext
Signals events to listeners:before: before-class methodsafter: test instantiationbefore: before methodsafter: after methodsafter: after-class methods
TestExecutionListener SPIReacts to test execution events
Receives reference to current TestContext
Out of the box:DependencyInjectionTestExecutionListenerDirtiesContextTestExecutionListenerTransactionalTestExecutionListener
TestExecutionListener
TEL: Prepare Instance
TEL: Befores and Afters
ContextLoader SPIStrategy for loading application contexts
from resource locations
Out of the box:GenericXmlContextLoaderGenericPropertiesContextLoader
ContextLoader 2.5
Putting it all together
New in Spring 3.1
Support for ...
testing with @Configuration classes
and
testing with environment profiles
made possible by ...
SmartContextLoader
AnnotationConfigContextLoader
DelegatingSmartContextLoader
updated context cache key generation
SmartContextLoader SPISupersedes ContextLoader
Strategy for loading applicationcontexts
From @Configuration classes orresource locations
Supports environment profiles
ImplementationsGenericXmlContextLoader
GenericPropertiesContextLoader
AnnotationConfigContextLoader
DelegatingSmartContextLoader
ContextLoader 2.5
ContextLoader 3.1
Testing with@Configuration Classes
@ContextConfigurationaccepts a new classes attribute...
@ContextConfiguration( classes={DataAccessConfig.class, ServiceConfig.class})
First, a review with XML config forcomparison...
OrderServiceTest-context.xml
<?xml version="1.0" encoding="UTF-8"?><beans ...> <!-- will be injected into OrderServiceTest --> <bean id="orderService" class="com.example.OrderServiceImpl"> <!-- set properties, etc. --> </bean> <!-- other beans --></beans>
OrderServiceTest.java
package com.example;@RunWith(SpringJUnit4ClassRunner.class)@ContextConfigurationpublic class OrderServiceTest { @Autowired private OrderService orderService; @Test public void testOrderService() { // test the orderService }}
Let's rework that example to use a@Configuration class and the newAnnotationConfigContextLoader...
OrderServiceTest.java
@RunWith(SpringJUnit4ClassRunner.class)
public class OrderServiceTest {
@Autowired private OrderService orderService; // @Test methods ...}
OrderServiceTest.java
@RunWith(SpringJUnit4ClassRunner.class)@ContextConfiguration( loader=AnnotationConfigContextLoader.class)public class OrderServiceTest {
@Autowired private OrderService orderService; // @Test methods ...}
OrderServiceTest.java
@RunWith(SpringJUnit4ClassRunner.class)@ContextConfigurationpublic class OrderServiceTest {
@Autowired private OrderService orderService; // @Test methods ...}
OrderServiceTest.java
@RunWith(SpringJUnit4ClassRunner.class)@ContextConfigurationpublic class OrderServiceTest { @Configuration static class Config {
} @Autowired private OrderService orderService; // @Test methods ...}
OrderServiceTest.java
@RunWith(SpringJUnit4ClassRunner.class)@ContextConfigurationpublic class OrderServiceTest { @Configuration static class Config { @Bean // will be injected into OrderServiceTest public OrderService orderService() { OrderService orderService = new OrderServiceImpl(); // set properties, etc. return orderService; } } @Autowired private OrderService orderService; // @Test methods ...}
What's changed?No XML
Bean definitions are converted toJava
using @Configuration and @Bean
Otherwise, the test remainsunchanged
But what if we don't want a staticinner @Configuration class?
Just externalize the config...
OrderServiceConfig.java
@Configurationpublic class OrderServiceConfig { @Bean // will be injected into OrderServiceTest public OrderService orderService() { OrderService orderService = new OrderServiceImpl(); // set properties, etc. return orderService; }}
And reference the config classes in@ContextConfiguration...
OrderServiceConfig.java
@RunWith(SpringJUnit4ClassRunner.class)@ContextConfiguration(classes=OrderServiceConfig.class)public class OrderServiceTest { @Autowired private OrderService orderService; @Test public void testOrderService() { // test the orderService }}
@Configuration + XMLQ: How can we combine@Configuration classes with XMLconfig?
A: Choose one as the entry point.
That's how it works in productionanyway
Importing ConfigurationIn XML:
include @Configuration classes viacomponent scanningor define them as normal Springbeans
In an @Configuration class:use @ImportResource to import XMLconfig files
Testing withEnvironment Profiles
@ActiveProfilesTo activate bean definition profiles in tests...
Annotate a test class with@ActiveProfiles
Supply a list of profiles to beactivated for the test
Can be used with @Configurationclasses and XML config
Let's look at an example with XMLconfig...
app-config.xml (1/2)
<beans ... > <bean id="transferService" class="com.example.DefaultTransferService"> <constructor-arg ref="accountRepository"/> <constructor-arg ref="feePolicy"/> </bean> <bean id="accountRepository" class="com.example.JdbcAccountRepository"> <constructor-arg ref="dataSource"/> </bean> <bean id="feePolicy" class="com.example.ZeroFeePolicy"/> <!-- dataSource --></beans>
app-config.xml (2/2)
<beans ... > <!-- transferService, accountRepository, feePolicy -->
</beans>
app-config.xml (2/2)
<beans ... > <!-- transferService, accountRepository, feePolicy --> <beans profile="production"> <jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/> </beans>
</beans>
app-config.xml (2/2)
<beans ... > <!-- transferService, accountRepository, feePolicy --> <beans profile="production"> <jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/> </beans> <beans profile="dev"> <jdbc:embedded-database id="dataSource"> <jdbc:script location="classpath:schema.sql"/> <jdbc:script location="classpath:test-data.sql"/> </jdbc:embedded-database> </beans></beans>
TransferServiceTest.java
@RunWith(SpringJUnit4ClassRunner.class)
public class TransferServiceTest { @Autowired private TransferService transferService; @Test public void testTransferService() { // test the transferService }}
TransferServiceTest.java
@RunWith(SpringJUnit4ClassRunner.class)@ContextConfiguration("/app-config.xml")public class TransferServiceTest { @Autowired private TransferService transferService; @Test public void testTransferService() { // test the transferService }}
TransferServiceTest.java
@RunWith(SpringJUnit4ClassRunner.class)@ContextConfiguration("/app-config.xml")@ActiveProfiles("dev")public class TransferServiceTest { @Autowired private TransferService transferService; @Test public void testTransferService() { // test the transferService }}
And now an example with@Configuration classes
TransferServiceConfig.java
@Configurationpublic class TransferServiceConfig { @Autowired DataSource dataSource; @Bean public TransferService transferService() { return new DefaultTransferService(accountRepository(), feePolicy()); } @Bean public AccountRepository accountRepository() { return new JdbcAccountRepository(dataSource); } @Bean public FeePolicy feePolicy() { return new ZeroFeePolicy(); }}
}
JndiDataConfig.java
@Configuration@Profile("production")public class JndiDataConfig { @Bean public DataSource dataSource() throws Exception { Context ctx = new InitialContext(); return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource"); }}
StandaloneDataConfig.java
@Configuration@Profile("dev")public class StandaloneDataConfig { @Bean public DataSource dataSource() { return new EmbeddedDatabaseBuilder() .setType(EmbeddedDatabaseType.HSQL) .addScript("classpath:schema.sql") .addScript("classpath:test-data.sql") .build(); }}
And finally the test class...
TransferServiceTest.java
package com.bank.service;@RunWith(SpringJUnit4ClassRunner.class)
public class TransferServiceTest { @Autowired private TransferService transferService; @Test public void testTransferService() { // test the transferService }}
TransferServiceTest.java
package com.bank.service;@RunWith(SpringJUnit4ClassRunner.class)@ContextConfiguration( classes={ TransferServiceConfig.class, StandaloneDataConfig.class, JndiDataConfig.class})public class TransferServiceTest { @Autowired private TransferService transferService; @Test public void testTransferService() { // test the transferService }}
TransferServiceTest.java
package com.bank.service;@RunWith(SpringJUnit4ClassRunner.class)@ContextConfiguration( classes={ TransferServiceConfig.class, StandaloneDataConfig.class, JndiDataConfig.class})@ActiveProfiles("dev")public class TransferServiceTest { @Autowired private TransferService transferService; @Test public void testTransferService() { // test the transferService }}
Active Profile Inheritance@ActiveProfiles supports inheritance as well
Via the inheritProfiles attribute
See Javadoc for an example
ApplicationContextCaching
Until Spring 3.1
application contexts were cached
but using only resource locations forthe key.
Now there are differentrequirements...
New Key Generation AlgorithmThe context cache key generation algorithm has been updated to include...
locations (from @ContextConfiguration)
classes (from @ContextConfiguration)
contextLoader (from @ContextConfiguration)
activeProfiles (from @ActiveProfiles)
SummaryThe Spring TestContext Framework simplifiesintegration testing of Spring-basedapplications
Spring 3.1 provides first-class testing supportfor:
@Configuration classesEnvironment profiles
See the Testing with @Configuration Classesand Profiles blog for further insight
Spring MVC TestSupport
How Do You Test an@Controller?
@Controller@RequestMapping("/accounts")public class AccountController { // ... @ModelAttribute public Account getAccount(String number) { return this.accountManager.getAccount(number); } @RequestMapping(method = RequestMethod.POST) public String save(@Valid Account account, BindingResult result) { if (result.hasErrors()) { return "accounts/edit"; } this.accountManager.saveOrUpdate(account); return "redirect:accounts"; }}
Unit Test?Create controller instance
Mock or stub services & repositories
Example@Testpublic void testSave() { Account account = new Account(); BindingResult result = new BeanPropertyBindingResult(account, "account");
}
Example@Testpublic void testSave() { Account account = new Account(); BindingResult result = new BeanPropertyBindingResult(account, "account"); AccountManager mgr = createMock(AccountManager.class); mgr.saveOrUpdate(account); replay(mgr);
}
Example@Testpublic void testSave() { Account account = new Account(); BindingResult result = new BeanPropertyBindingResult(account, "account"); AccountManager mgr = createMock(AccountManager.class); mgr.saveOrUpdate(account); replay(mgr); AccountController contrlr = new AccountController(mgr); String view = contrlr.save(account, result);
}
Example@Testpublic void testSave() { Account account = new Account(); BindingResult result = new BeanPropertyBindingResult(account, "account"); AccountManager mgr = createMock(AccountManager.class); mgr.saveOrUpdate(account); replay(mgr); AccountController contrlr = new AccountController(mgr); String view = contrlr.save(account, result); assertEquals("redirect:accounts", view); verify(mgr);}
Example With Failure@Testpublic void testSave() { Account account = new Account(); BindingResult result = new BeanPropertyBindingResult(account, "account"); result.reject("error.code", "default message")
}
Example With Failure@Testpublic void testSave() { Account account = new Account(); BindingResult result = new BeanPropertyBindingResult(account, "account"); result.reject("error.code", "default message") AccountManager mgr = createMock(AccountManager.class); replay(mgr);
}
Example With Failure@Testpublic void testSave() { Account account = new Account(); BindingResult result = new BeanPropertyBindingResult(account, "account"); result.reject("error.code", "default message") AccountManager mgr = createMock(AccountManager.class); replay(mgr); AccountController contrlr = new AccountController(mgr); String view = contrlr.save(account, result); assertEquals("accounts/edit", view); verify(mgr);}
Not Fully TestedRequest mappings
Binding errors
Type conversion
Etc.
Integration Test?Selenium
JWebUnit
etc.
It Requires...A running servlet container
More time to execute
More effort to maintain
Server is a black box
Actually...
it's an end-to-end test
the only way to verify...Client-side behavior
Interaction with other server instances
Redis, Rabbit, etc.
We'd also like to...Test controllers once!
Fully & quickly
Execute tests often
In summary...We can't replace the need for end-to-
end tests
But we can minimize errors
Have confidence in @MVC code
During end-to-end tests
Spring MVC TestBuilt on spring-test
No Servlet container
Drives Spring MVC infrastructure
Both server & client-side test support (i.e. RestTemplate code)
Inspired by spring-ws-test
The ApproachRe-use controller test fixtures
I.e. mocked services & repositories
Invoke @Controller classes through@MVC infrastructure
ExampleString contextLoc = "classpath:appContext.xml";String warDir = "src/main/webapp";MockMvc mockMvc = xmlConfigSetup(contextLoc) .configureWebAppRootDir(warDir, false) .build();mockMvc.perform(post("/persons")) .andExpect(response().status().isOk()) .andExpect(response().forwardedUrl("/add.jsp")) .andExpect(model().size(1)) .andExpect(model().hasAttributes("person"));
Under the CoversMock request/response from spring-test
DispatcherServlet replacement
Multiple ways to initialize MVCinfrastructure
Save results for expectations
Ways To Initialize MVCInfrastructure
From Existing XML Config// XML configMockMvc mockMvc = xmlConfigSetup("classpath:appContext.xml") .activateProfiles(..) .configureWebAppRootDir(warDir, false) .build();
From Existing Java Config// Java configMockMvc mockMvc = annotationConfigSetup(WebConfig.class) .activateProfiles(..) .configureWebAppRootDir(warDir, false) .build();
Manually MockMvc mockMvc = standaloneSetup(new PersonController()) .setMessageConverters(..) .setValidator(..) .setConversionService(..) .addInterceptors(..) .setViewResolvers(..) .setLocaleResolver(..) .build();
About Manual SetupMVC components instantiated directly
Not looked up in Spring context
Focused, readable tests
But must verify MVC config separately
TestContext Framework Example@RunWith(SpringJUnit4ClassRunner.class)@ContextConfiguration( locations={"/org/example/servlet-context.xml"})public class TestContextTests { @Autowired private WebApplicationContext wac; @Before public void setup() { MockMvc mockMvc = MockMvcBuilders.webApplicationContextSetup(this.wac) .build(); }}
TestContext FrameworkCaches loaded Spring configuration
Potentially across all tests!
However...WebApplicationContext not supported yet
To be supported in Spring 3.2
In the mean time...You can use a custom ContextLoader
Example exists in spring-test-mvc
Step 1Add static imports
MockMvcBuilders.*
MockMvcRequestBuilders.*
MockMvcResultActions.*
Easy to remember...MockMvc*
Also in Eclipse...Add MockMvc* classes in Preferences
Java -> Editor -> Favorites
Helps content assist
Step 2Initialize MVC infrastructure
String contextLoc = "classpath:appContext.xml";String warDir = "src/main/webapp";MockMvc mockMvc = xmlConfigSetup("classpath:appContext.xml") .configureWebAppRootDir(warDir, false) .build()
// ...
Step 3Build Request
String contextLoc = "classpath:appContext.xml";String warDir = "src/main/webapp";MockMvc mockMvc = xmlConfigSetup("classpath:appContext.xml") .configureWebAppRootDir(warDir, false) .build()mockMvc.perform(get("/").accept(MediaType.APPLICATION_XML))
// ...
Step 4Add Expectations
String contextLoc = "classpath:appContext.xml";String warDir = "src/main/webapp";MockMvc mockMvc = xmlConfigSetup("classpath:appContext.xml") .configureWebAppRootDir(warDir, false) .build()mockMvc.perform(get("/").accept(MediaType.APPLICATION_XML)) .andExpect(response().status().isOk()) .andExpect(response().contentType(MediaType)) .andExpect(response().content().xpath(String).exists()) .andExpect(response().redirectedUrl(String)) .andExpect(model().hasAttributes(String...));
// ...
Step 5Debug
String contextLoc = "classpath:appContext.xml";String warDir = "src/main/webapp";MockMvc mockMvc = xmlConfigSetup("classpath:appContext.xml") .configureWebAppRootDir(warDir, false) .build()mockMvc.perform(get("/").accept(MediaType.APPLICATION_XML)) .andPrint(toConsole());
// ...
Demo
https://github.com/SpringSource/spring-test-mvc
Sample Tests:
org.springframework.test.web.server.samples
Client-Side ExampleRestTemplate restTemplate = ... ;MockRestServiceServer.createServer(restTemplate) .expect(requestTo(String)) .andExpect(method(HttpMethod)) .andExpect(body(String)) .andExpect(headerContains(String, String)) .andRespond(withResponse(String, HttpHeaders));
Project Availabilitygithub.com/SpringSource/spring-test-mvc
Request for feedback!
Send comments
Pull requests
Nightly Snapshot Available<repository> <id>org.springframework.maven.snapshot</id> <name>Spring Maven Snapshot Repository</name> <url>http://maven.springframework.org/snapshot</url> <releases> <enabled>false</enabled> </releases> <snapshots> <enabled>true</enabled> </snapshots></repository>
In Closing ...
This PresentationSource:https://github.com/rstoyanchev/spring-31-and-mvc-testing-support
Slides:http://rstoyanchev.github.com/spring-31-and-mvc-test
Resources for Spring MVC TestProject Home:https://github.com/SpringSource/spring-test-mvc
Sample Tests:org.springframework.test.web.server.samples
Resources for Core SpringSpring Framework:http://www.springsource.org/spring-core
Reference Manual: Spring 3.1 RC1
Forums: http://forum.springframework.org
JIRA: https://jira.springsource.org
Spring in a Nutshell … available in 2012
Q&A
Sam BrannenWeb: Swiftmind.com
Twitter: @sam_brannen
Rossen StoyanchevWeb: SpringSource.com
Twitter: @rstoya05