I have set up spring boot application using Gradle. Now I do understand that #EnableAutoConnfiguration configures the application based on dependencies in a class path. I am pretty happy to avoid all of the plumbing but things start happening which I wish wouldn't.
Here are my dependencies:
dependencies {
compile('org.springframework.boot:spring-boot-starter-web:1.2.3.RELEASE')
compile 'org.springframework.hateoas:spring-hateoas:0.17.0.RELEASE'
compile 'org.springframework.plugin:spring-plugin-core:1.2.0.RELEASE'
compile 'org.springframework.boot:spring-boot-starter-data-jpa'
compile 'com.google.guava:guava:18.0'
compile 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310'
compile 'commons-beanutils:commons-beanutils:1.9.2'
runtime 'org.hsqldb:hsqldb:2.3.2'
testCompile 'org.springframework.boot:spring-boot-starter-test'
testCompile 'com.jayway.jsonpath:json-path:2.0.0'
}
My application class:
#ComponentScan("org.home.project")
#SpringBootApplication
//#EnableHypermediaSupport(type = EnableHypermediaSupport.HypermediaType.HAL)
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
A snippet from UserController:
#RequestMapping(method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE)
public HttpEntity<ResourceSupport> create(#Valid #RequestBody UserCreateRequest ucr, BindingResult bindingResult) {
if (bindingResult.hasErrors()) throw new InvalidRequestException("Bad Request", bindingResult);
Long userId = userService.create(ucr);
ResourceSupport resource = new ResourceSupport();
resource.add(linkTo(UserEndpoint.class).withSelfRel());
resource.add(linkTo(methodOn(UserEndpoint.class).update(userId, null, null)).withRel(VIEW_USER));
resource.add(linkTo(methodOn(UserEndpoint.class).delete(userId)).withRel(DELETE_USER));
return new ResponseEntity(resource, HttpStatus.CREATED);
}
The UserController.java has two annotations:
#RestController
#RequestMapping(value = "/users", produces = MediaType.APPLICATION_JSON_VALUE)
First of - notice the commented out #EnableHyperdiaSupport annotation - links in the ResourceSupport instance are still serialized to hal+json format despite media type produced and media type set in the request. This happens automatically when 'org.springframework.plugin:spring-plugin-core:1.2.0.RELEASE' is introduced in the dependencies. How would one go about configuring it explicitly ?
Another issue are unit tests.
This passes:
#RunWith(SpringJUnit4ClassRunner.class)
#SpringApplicationConfiguration(classes = MockServletContext.class)
#WebAppConfiguration
public class UserControllerTest {
...ommited for brevity...
#InjectMocks
private UserController testObject;
#Before
public void setUp() throws Exception {
initMocks(this);
mockMvc = standaloneSetup(testObject).build();
}
#Test
public void testUserCreatedLinks() throws Exception {
mockMvc.perform(post("/users").contentType(MediaType.APPLICATION_JSON).content(data))
.andExpect(status().isCreated())
.andExpect(content().contentType(MediaType.APPLICATION_JSON)).andExpect(jsonPath("$.links.[*].rel", hasItem("self")));
}
...ommited fro brevity...
}
The post request returns a standard JSON response in the test - not HAL+JSON. Is there a way to reconfigure this so that unit testing #RestController with MockServletContext would produce HAL+JSON or getting back to problem number 1 - how to configure the response format explicitly so that Jackson serializer would not produce hal+json ?
You're running your test using Spring MVC Test's standaloneSetup which uses a bare minimum of configuration to get your controller up and running. That configuration isn't the same as the configuration that will be used when you run the whole application.
If you want to use the same configuration, you could use webAppContextSetup:
#RunWith(SpringJUnit4ClassRunner.class)
#SpringApplicationConfiguration(classes = Application.class)
#WebAppConfiguration
public class SomeTests {
#Autowired
private WebApplicationContext context;
private MockMvc mockMvc;
#Before
public void setUp() {
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context).build();
}
}
Alternatively, you can replicate Spring HATEOAS's configuration in the standalone setup. Note that this runs the risk of your tests' configuration deviating from your application's configuration. You'd create the MockMvc instance like this:
TypeConstrainedMappingJackson2HttpMessageConverter messageConverter =
new TypeConstrainedMappingJackson2HttpMessageConverter(ResourceSupport.class);
messageConverter.setSupportedMediaTypes(Arrays.asList(MediaTypes.HAL_JSON));
ObjectMapper objectMapper = messageConverter.getObjectMapper();
objectMapper.registerModule(new Jackson2HalModule());
objectMapper.setHandlerInstantiator(
new Jackson2HalModule.HalHandlerInstantiator(new DefaultRelProvider(), null));
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(testObject)
.setMessageConverters(messageConverter).build();
Related
today I've met online another poor soul learning Spring. I decided I'll help them. Story as old as Spring, a missing bean in unit tests. I made a quick fix, I put a configuration with the missing bean and it worked, seemed like everything was fine.
#Configuration
class Config {
#Bean
HelloService getHelloService() {
return new HelloService();
}
}
#ExtendWith(SpringExtension.class)
#WebMvcTest(HelloController.class)
#Import({Config.class})
class HelloControllerIntTest {
#Autowired
private MockMvc mvc;
#Test
void hello() throws Exception {
RequestBuilder request = get("/hello");
MvcResult result = mvc.perform(request).andReturn();
assertEquals("Hello, World", result.getResponse().getContentAsString());
}
#Test
public void testHelloWithName() throws Exception {
mvc.perform(get("/hello?name=Dan"))
.andExpect(content().string("Hello, Dan"));
}
}
On the second thought, polluting the public space with additional and very genericly named class is not a good idea, so I decided to put it inside of the class.
#ExtendWith(SpringExtension.class)
#WebMvcTest(HelloController.class)
#Import({HelloControllerIntTest.Config.class})
class HelloControllerIntTest {
#Configuration
static class Config {
#Bean
HelloService getHelloService() {
return new HelloService();
}
}
#Autowired
private MockMvc mvc;
#Test
void hello() throws Exception {
RequestBuilder request = get("/hello");
MvcResult result = mvc.perform(request).andReturn();
assertEquals("Hello, World", result.getResponse().getContentAsString());
}
#Test
public void testHelloWithName() throws Exception {
mvc.perform(get("/hello?name=Dan"))
.andExpect(content().string("Hello, Dan"));
}
}
To my surprise, it doesn't work, 404 error. I put a breakpoint in the HelloController and it seems the bean is not constructed at all. Also I peeked into the beans definitions and it seems the first version has 91 beans, and the second 88, so we have missing beans over there.
Any ideas what happened here? Why in the second version Spring ignores HelloController?
The reason why this happens is because your Config annotation is looking in a sub-package to find the beans but it can no longer find them.
If you annotate your Config static class with a #ComponentScan("package.of.your.helloController") then your controller it will be found again.
I tried junit with mockito, and wrote some test cases for a coding exercise.
Here is the test case which i wrote:
#RunWith(SpringRunner.class)
public class TransactionControllerTest {
#Mock
TransactionService transactionServiceMock;
#InjectMocks
TransactionController transactionController;
TransactionRequest txn = new TransactionRequest("123.34", "2018-11-28T23:32:36.312Z");
#Test
public void testSaveTxn() throws Exception {
Mockito.when(transactionServiceMock.saveTxn(Mockito.any(TransactionRequest.class))).thenReturn(true);
ResponseEntity<?> responseEntity = transactionController.saveTxn(null, txn);
assertTrue(responseEntity.getStatusCode().equals(HttpStatus.CREATED));
}
#Test
public void testGetStats() throws Exception {
StatsResponse sr = new StatsResponse("0.00", "0.00", "0.00", "0.00", 0L);
Mockito.when(transactionServiceMock.getStats()).thenReturn(sr);
ResponseEntity<StatsResponse> responseEntity = (ResponseEntity<StatsResponse>) transactionController.getStats(null);
System.out.println("sr response = "+responseEntity.getBody());
assertTrue(responseEntity.getBody().equals(sr));
}
#Test
public void testDelete() throws Exception {
Mockito.doNothing().when(transactionServiceMock).delete();
ResponseEntity<HttpStatus> responseEntity = (ResponseEntity<HttpStatus>) transactionController.deleteTxn(null);
System.out.println("sr response = "+responseEntity.getBody());
assertTrue(responseEntity.getStatusCode().equals(HttpStatus.NO_CONTENT));
}
}
The test cases were working fine.
But my application was rejected specifying the following reason:
You were using SpringRunner even though you are not using SpringContext in the tests, and mocking everything.
Now, following are my concerns:
What's wrong with the test cases?
What is the meaning of above rejection reason?
How can i correct that?
What's wrong with the test cases?
I think what they want you to do is to write a spring web layer test. This is not a spring MVC test/spring-boot test. Because you don't test the controller as a spring loaded resource. You test it as a simple java class. That won't prove whether it behaves as a Spring controller correctly. You won't be able to test features such as;
spring resource injection
request dispatching and validation
How can i correct that?
You have to write a spring MVC test and use MockMvc or RestTemplate to verify your controller. For example;
#RunWith(SpringJUnit4ClassRunner.class)
#ContextConfiguration(classes = YourContext.class)
#WebAppConfiguration
public class MyWebTests {
#Autowired
private WebApplicationContext wac;
private MockMvc mockMvc;
#Before
public void setup() {
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
}
#Test
public void foo() throws Exception {
mockMvc.perform(get("/status"));
//and verification
}
}
Usage of mockito mocks is not the worst idea, but you could have used auto wired #MockBeans.
If this is spring-boot, you will have more flexibility. Have a look at following resources.
https://docs.spring.io/spring/docs/current/spring-framework-reference/testing.html
https://spring.io/guides/gs/testing-web/
You have complaint because you don't need spring's test features in your test.
Your test is pure unit test.
So if you will remove #RunWith(SpringRunner.class) nothing will be changed for your test. Just put there #ExtendWith(MockitoExtension.class)
SpringRunner will initialize spring context for you test that you could inject or mock slice of your application using following annotations:
#MockBean
#Autowired
etc..
Is there any tool or maven plugin that can validate Spring context during compile time or maven build execution?
I understand that it's not trivial to check the full correctness of the context without the app start, but it will be good to check some trivial cases, for example if you define a bean in xml context then the bean class have to be present in classpath.
Every Spring Guide contains such sanity test.
For Spring MVC should test with MockMvc test. To verify that Spring configuration is OK, you can Create full context and fire requests against URL and cover also validation + all the Spring wiring. Such test is executed during test maven phase.
Something like this:
#WebAppConfiguration
#ContextConfiguration(classes = RestApplication.class)
public class RestApplicationContextTest extends
AbstractTestNGSpringContextTests {
private static final String FULL_USER_URL = "http://localhost:10403/users";
private MockMvc mockMvc;
#Autowired
private WebApplicationContext webApplicationContext;
#BeforeMethod
public void init() {
mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
}
private static String createTestRecord(int identifier) {
String testingRecordString =
"{\"email\": \"user%d#gmail.com\", \"name\": \"User%d\"}";
return String.format(testingRecordString, identifier, identifier,
identifier);
}
#Test
public void testPost() throws Exception {
// GIVEN
String testingRecord = createTestRecord(0);
// WHEN
// #formatter:off
MvcResult mvcResult = mockMvc.perform(post(FULL_USER_URL)
.contentType(MediaType.APPLICATION_JSON)
.content(testingRecord))
.andReturn();
// #formatter:on
// THEN
int httpStatus = mvcResult.getResponse().getStatus();
assertEquals(httpStatus, HttpStatus.CREATED.value());
}
...
I'm using Spring Boot 1.2.5-RELEASE. I have a controller that receive a MultipartFile and a String
#RestController
#RequestMapping("file-upload")
public class MyRESTController {
#Autowired
private AService aService;
#RequestMapping(method = RequestMethod.POST, consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
#ResponseStatus(HttpStatus.CREATED)
public void fileUpload(
#RequestParam(value = "file", required = true) final MultipartFile file,
#RequestParam(value = "something", required = true) final String something) {
aService.doSomethingOnDBWith(file, value);
}
}
Now, the service works well. I tested it with PostMan and eveything goes as expected.
Unfortunately, I cannot write a standalone unit test for that code. The current unit test is:
#RunWith(SpringJUnit4ClassRunner.class)
#SpringApplicationConfiguration(classes = MyApplication.class)
#WebAppConfiguration
public class ControllerTest{
MockMvc mockMvc;
#Mock
AService aService;
#InjectMocks
MyRESTController controller;
#Before public void setUp(){
MockitoAnnotations.initMocks(this);
this.mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
}
#Test
public void testFileUpload() throws Exception{
final File file = getFileFromResource(fileName);
//File is correctly loaded
final MockMultipartFile multipartFile = new MockMultipartFile("aMultiPartFile.txt", new FileInputStream(file));
doNothing().when(aService).doSomethingOnDBWith(any(MultipartFile.class), any(String.class));
mockMvc.perform(
post("/file-upload")
.requestAttr("file", multipartFile.getBytes())
.requestAttr("something", ":(")
.contentType(MediaType.MULTIPART_FORM_DATA_VALUE))
.andExpect(status().isCreated());
}
}
Test fails with
java.lang.IllegalArgumentException: Expected MultipartHttpServletRequest: is a MultipartResolver configured?
Now, in the MultipartAutoConfiguration class from Spring Boot I see that a MultipartResolver is auto configured. But, I guess that with the standaloneSetup of MockMvcBuilders I cannot access this.
I tried several configurations of the unit test that I don't report for brevity. Especially, I also tried rest-assured as shown here, but honestly this doesn't work because it seems that I cannot mock the AService instance.
Any solution?
You are trying to combine here unit test (standaloneSetup(controller).build();) with Spring integration test (#RunWith(SpringJUnit4ClassRunner.class)).
Do one or the other.
Integration test will need to use something like code below. The problem would be faking of beans. There are ways to fake such bean with #Primary annotation and #Profile annotation (you create testing bean which will override main production bean). I have some examples of such faking of Spring beans (e.g. this bean is replaced by this bean in this test).
#Autowired
private WebApplicationContext webApplicationContext;
#BeforeMethod
public void init() {
mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
}
Secodn option is to remove #RunWith(SpringJUnit4ClassRunner.class) and other class level configuration on your test and test controller without Spring Context with standalone setup. That way you can't test validation annotations on your controller, but you can use Spring MVC annotations. Advantage is possibility to fake beans via Mockito (e.g. via InjectMocks and Mock annotations)
I mixed what lkrnak suggested and Mockito #Spy functionality. I use REST-Assured to do the call. So, I did as follows:
#RunWith(SpringJUnit4ClassRunner.class)
#SpringApplicationConfiguration(classes = MyApplication.class)
#WebAppConfiguration
#IntegrationTest({"server.port:0"})
public class ControllerTest{
{
System.setProperty("spring.profiles.active", "unit-test");
}
#Autowired
#Spy
AService aService;
#Autowired
#InjectMocks
MyRESTController controller;
#Value("${local.server.port}")
int port;
#Before public void setUp(){
RestAssured.port = port;
MockitoAnnotations.initMocks(this);
}
#Test
public void testFileUpload() throws Exception{
final File file = getFileFromResource(fileName);
doNothing().when(aService)
.doSomethingOnDBWith(any(MultipartFile.class), any(String.class));
given()
.multiPart("file", file)
.multiPart("something", ":(")
.when().post("/file-upload")
.then().(HttpStatus.CREATED.value());
}
}
the service is defined as
#Profile("unit-test")
#Primary
#Service
public class MockAService implements AService {
//empty methods implementation
}
The error says the request is not a multi-part request. In other words at that point it's expected to have been parsed. However in a MockMvc test there is no actual request. It's just mock request and response. So you'll need to use perform.fileUpload(...) in order to set up a mock file upload request.
This basic Spring test of a Spring 3 controller gives me a response code 404 result instead of the expected 200:
#RunWith(SpringJUnit4ClassRunner.class)
public class RootControllerMvcTest extends AbstractContextControllerTests {
private MockMvc mockMvc;
#Before
public void setup() throws Exception {
this.mockMvc = webAppContextSetup(this.wac)
.alwaysExpect(status().isOk()).build();
}
#Test
public void viewIndex() throws Exception {
this.mockMvc.perform(get("/")).andExpect(view()
.name(containsString("index"))).andDo(print());
}
AbstractContextControllerTests:
#WebAppConfiguration("file:src/main/webapp/WEB-INF/spring/webmvc-config.xml")
#ContextConfiguration("file:src/main/resources/META-INF/spring/applicationContext.xml")
public class AbstractContextControllerTests {
#Autowired
protected WebApplicationContext wac; }
I have verified the controller method itself with another test, but when I use the context the test fails even as the controller does serve the proper page when run in container.
The controller in question looks like this:
#Controller
public class RootController {
#Autowired
CategoryService categoryService;
#RequestMapping(value = "/", method = RequestMethod.GET, produces = "text/html")
public String index(Model uiModel) {
uiModel.addAttribute("categories", categoryService.findAll());
return "index";
}
Clearly I'm not testing what I think. Any suggestion how to triangulate this issue?
I posted the full web mvc file at Pastebin to not clutter all space up here.
FYI: The value attribute for #WebAppConfiguration is not an XML configuration file but rather the root directory of your web application. So your current test configuration could never work.
Assuming that applicationContext.xml and webmvc-config.xml are the XML configuration files for your root and DispatcherServlet WebApplicationContexts, respectively, try redefining AbstractContextControllerTests as follows:
#RunWith(SpringJUnit4ClassRunner.class)
#WebAppConfiguration
#ContextHierarchy ({
#ContextConfiguration("/META-INF/spring/applicationContext.xml"),
#ContextConfiguration("file:src/main/webapp/WEB-INF/spring/webmvc-config.xml")
})
public abstract class AbstractContextControllerTests {
#Autowired
protected WebApplicationContext wac;
}
By the way, abstract test classes must actually be declared as abstract. ;)
Regards,
Sam (author of the Spring TestContext Framework)