Unit Testing Spring Boot API RESTful endpoints generated by Open API 3 YAML files - spring-boot

I have an application that is using Spring Boot (latest version) and creating a back-end that has RESTful api's. Traditionally, I have created controllers like:
#RestController
#RequestMapping("/contacts")
public class ContactController {
#Autowired
private ContactService service;
#RequestMapping(value = "/contactId/{contactId}",
method = RequestMethod.GET, headers = "Accept=application/json")
public #ResponseBody ContactEntity getContactById(#PathVariable("contactId") long contactId) {
ContactEntity contactEntity = service.getContactById(contactId);
return contactEntity;
}
And an integrated test has always been like:
#ExtendWith(SpringExtension.class)
#SpringBootTest(classes = ServiceContextConfiguration.class)
#ComponentScan("com.tomholmes.springboot.phonebook.server")
#Transactional
#WebAppConfiguration
public class ContactControllerTest {
#Test
public void testGetContactById() throws Exception {
MockHttpServletRequestBuilder requestBuilder =
MockMvcRequestBuilders.get(BASE_URL + "/contactId/6");
this.mockMvc.perform(requestBuilder)
.andDo(print())
.andExpect(status().isOk());
}
}
This has always worked normally for years as a 'code first' api. Now, I am dealing with a contract-first API using OpenAPI 3 and a YAML file. The API is generated in the same location as before, and I would expect the test to work as it did before, but it does not.
So one resource:
[https://www.hascode.com/2018/08/testing-openapi-swagger-schema-compliance-with-java-junit-and-assertj-swagger/#API_Test]
is suggesting I use the assertj-swagger for the OpenAPI / Swagger contract testing.
Is this the only way to go? Is there no way for me to use my old traditional testing which I find extremely useful as an integrated test.
There is a third method I am also looking into:
[https://www.testcontainers.org/modules/mockserver/]
Which I am going to try also, and I am sure it will work.
I'm also wondering if there is code out there to auto-generate the Test just like there is to generate the API endpoint and the model, it would make sense if the Open API 3 also had the ability to generate the test was well.
Ultimately, I'd like to use my old way of testing if I could, but if not, then I'll try the other ways.

We had the same issue after switching to open api contract first with auto-generated controllers/delegate interfaces. The fix was to import the delegate impl in addition to the controller itself. Example:
#WebMvcTest(FeatureController.class)
#Import(FeatureDelegateImpl.class)
public class FeatureContractTest {
#Autowired
private MockMvc mvc;
#MockBean
private BusinessService businessService;
...
FeatureController is the controller generated by open-api, which typically will be in a generated-sources folder within target. FeatureDelegateImpl is our own implementation of the delegate interface, which lives in the src/main folder.

Related

Springboot test controller via WebClient instead of WebTestClient

I'm attempting something perhaps misguided but please help.
I would like to test springboot controller via #WebFluxTest. However I would like to use WebClient instead of WebTestClient. How can this be done?
So far, I managed to use reflection to get ExchangeFunction out of WebTestClient and assign it to WebClient - and it works! Calls are made, controller responds - wonderful. However I don't think this is good approach. Is there a better way?
Thank you.
Ideally, you should use a WebTestClient which is more of a convenience wrapper around the WebClient. Just like the TestRestTemplate is for a RestTemplate. Both allow a request to be created and you can easily make assertions. They exist to make your test life easier.
If you really want to use a WebClient instead of a WebTestClient and do the assertions manually (which means you are probably complicating things) you can use the WebClient.Builder to create one. Spring Boot will automatically configure one and you can simply autowire it in your test and call the build method.
#SpringBootTest
public void YourTest {
#Autowired
private WebClient.Builder webClientBuilder;
#Test
public void doTest() {
WebClient webClient = webClientBuilder.build();
}
}
The same should work with #WebFluxTest as well.
Ok, after much experimentation here is a solution to test springboot controller & filters via a mocked connection - no webservice, no ports and quick test.
Unfortunately I didn't work out how to do it via #WebFluxTest and WebClient, instead MockMvc can be used to achieve desired result:
#ExtendWith(SpringExtension.class)
#Import({SomeDependencyService.class, SomeFilter.class})
#WebMvcTest(controllers = SomeController.class, excludeAutoConfiguration = SecurityAutoConfiguration.class)
#AutoConfigureMockMvc()
public class SomeControllerTest {
#MockBean
private SomeDependencyService someDependencyService;
#Autowired
private MockMvc mockMvc;
private SomeCustomizedClient subject;
#BeforeEach
public void setUp() {
subject = buildClient();
WebClient webClient = mockClientConnection();
subject.setWebClient(webClient);
}
private WebClient mockClientConnection() {
MockMvcHttpConnector mockMvcHttpConnector = new MockMvcHttpConnector(mockMvc);
WebClient webClient = WebClient.builder().clientConnector(mockMvcHttpConnector).build();
return webClient;
}
#Test
public void sample() {
when(SomeDependencyService.somePersistentOperation(any(), any())).thenReturn(new someDummyData());
SomeDeserializedObject actual = subject.someCallToControllerEndpoint("example param");
assertThat(actual.getData).isEquals("expected data");
}
}
Now it is possible to test your customized client (for example if you have internal java client that contains few important customization like security, etags, logging, de-serialization and uniform error handling) and associated controller (and filters if you #import them along) at the cost of a unit test.
You do NOT have to bring up entire service to verify the client and controller is working correctly.

Spring MockMvc not taking roles into account

I have API endpoints which require a user to hold a specific role. Therefore, in some of my tests I attempt to reach these endpoints and expect a 401 error, however I get 200. I am using MockMvc to perform the calls.
The following are some snippets of the controller class with one of the methods that I am testing:
#RestController
public class MyController {
#GetMapping("/getcurrentuser")
public User getCurrent() {
...code
}
}
The following is my test class (only showing the respective test method and variables):
#RunWith(SpringRunner.class)
#WebMvcTest(MyController.class)
#ContextConfiguration(classes = MyController.class)
public class MyControllerTest {
#Autowired
private MockMvc mockMvc;
#Test
public void testGetCurrentFailedDueToIncorrectRole() throws Exception {
mockMvc.perform(get("/api/getcurrentuser")
.with(user(USER_NAME).password(PASSWORD)))
.andExpect(status().isUnauthorized());
}
}
I have also have a spring security config class, however I'm not sure if it's being brought into context in this test (sorry I'm still fairly new to spring and unit testing). Inside this class I have the following line of code:
.antMatchers("/api/**").hasAnyRole("ADMIN", "READ_ONLY")
The test showed previously fails, as I said I get 200. Now at this point I think that I'm doing something wrong in the configuration of this test and that is why roles are not being accounted for. Or maybe I am confused on how the ".with" part works.
Any form of help would be appreciated.
If you are using Spring Boot, you might want to try using #SpringBootTest and #AutoConfigureMockMvc.
https://spring.io/guides/gs/testing-web/
Exact opposite problem (may be useful to go off of)

Spring Boot - Test for controller fails with 404 code

I want to write a test for controller. Here is test snippet:
#RunWith(SpringRunner.class)
#WebMvcTest(WeatherStationController.class)
#ContextConfiguration(classes = MockConfig.class)
public class WeatherStationControllerTest {
#Autowired
private MockMvc mockMvc;
#Autowired
private IStationRepository stationRepository;
#Test
public void shouldReturnCorrectStation() throws Exception {
mockMvc.perform(get("/stations")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk());
}
}
controller code snippet:
#RestController
#RequestMapping(value = "stations")
public class WeatherStationController {
#Autowired
private WeatherStationService weatherService;
#RequestMapping(method = RequestMethod.GET)
public List<WeatherStation> getAllWeatherStations() {
return weatherService.getAllStations();
}
#RequestMapping(value = "/{id}", method = RequestMethod.GET)
public WeatherStation getWeatherStation(#PathVariable String id) {
return weatherService.getStation(id);
}
MockConfig class:
#Configuration
#ComponentScan(basePackages = "edu.lelyak.repository")
public class MockConfig {
//**************************** MOCK BEANS ******************************
#Bean
#Primary
public WeatherStationService weatherServiceMock() {
WeatherStationService mock = Mockito.mock(WeatherStationService.class);
return mock;
}
Here is error stack trace:
java.lang.AssertionError: Status
Expected :200
Actual :404
I can get what is wrong here.
How to fix test for controller?
HTTP code 404, means no resource found (on the server) for your request, which I think that your controller is not visible(let me say is not scanned) by spring boot.
A simple solution is scanning a parent package in MockConfig class, so spring can pick up all beans,
#ComponentScan(basePackages = "edu.lelyak") // assuming that's the parent package in your project
if you don't like this approach, you can add the controller's package name in basePackages
#ComponentScan(basePackages = {"edu.lelyak.controller","edu.lelyak.repository")
BTW, you don't have to manually set up WeatherStationService in MockConfig class, Spring boot can inject a mock for you and automatically reset it after each test method, you should just declare it in your test class:
#MockBean
private IStationRepository stationRepository;
On the other hand, you should mock weatherService.getAllStations() before calling get("/stations") in your test method (as you're not running integration test), so you can do:
List<WeatherStation> myList = ...;
//Add element(s) to your list
Mockito.when(stationService.getAllStations()).thenReturn(myList);
You can find more in :
Testing improvements in Spring Boot 1.4
Spring Boot features: Testing
I had the same issue. The controller was not getting picked up despite specifying it with #WebMvcTest(MyController.class). This meant all of its mappings were ignored, causing the 404. Adding #Import(MyController.class) resolved the issue, but I didn't expect the import to be necessary when I'm already specifying which controller to test.
I am not sure why your test is not working. But I got another solution which works for me.
#SpringBootTest
public class ControllerTest {
#Autowired
private MockMvc mockMvc;
#Before
public void setup() {
this.mockMvc = MockMvcBuilders.standaloneSetup(new TestController()).build();
}
#Test
public void shouldReturnCorrectStation() throws Exception {
mockMvc.perform(get("/stations")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk());
}
}
After some debugging, it appears that the target controller is simply not registered as a method handler. Spring scans the beans for the presence of RestController annotation.
But the problem is that the annotation could be found only if the bean is proxied via CGLIB, but for the case when we use WebMvcTest it's proxied by JDK.
As a result, I searched for the configuration which is responsible for making the choice, and the one was finally found AopAutoConfiguration. So when SpringBootTest is used this one is autoloaded when you need WebMvcTest+PreAuthorize in your controllers, then simply use:
#Import(AopAutoConfiguration.class)
I import external configuration class by #ContextConfiguration(classes = MyConfig.class)
When I changed in MyConfig annotation #Configuration into #TestConfiguration it started to work properly.
I couldn't find a good answer but I could find one of the causes.
I was using in my tests the #PreAuthorize on the RestController.
You can mock the Oauth with this tip on the integration tests that use SpringBootTest. For SpringBootTest, this works very well too, but using SpringBootTest you load a lot of other resources (like JPA) that is not necessary to do a simple Controller test.
But with #WebMvcTest this not works as expected. The use of the WithMockOAuth2Scope annotation can be enough to stop the 401 error from authentication problem, but after that the WebMvcTest can't find the rest endpoint, returning the 404 error code.
After removing the #PreAuthorize on Controller, the test with WebMvcTest pass.
Based on the accepted answer, in my case I had copied and modified the file based on another test, but forgot to change the name for the controller on the top of the class, that being the reason why it was not finding the resource, as the error says.
#RunWith(SpringRunner.class)
#WebMvcTest(AnswerCommandController.class)
public class AnswerCommandControllerTest {
Here is a different approach to the controller test that worked for me.
Assumption: The class WeatherStationService is a #SpringBootApplication
Then, the test class below should work for you:
#RunWith(SpringRunner.class)
#SpringApplicationConfiguration(WeatherStationService.class)
#WebIntegrationTest
public class WeatherStationControllerTest {
#Autowired
private WebApplicationContext context;
MockMvc mockMvc;
#Before
public void setup() {
mockMvc = MockMvcBuilders.webAppContextSetup(this.context).build();
}
#Test
public void shouldReturnCorrectStation() throws Exception {
mockMvc.perform(get("/stations")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk();
}
}
With this test setup, you should no longer need the MockConfig class.
In my case it was about a missing starting slash /
I've appended / to both RequestMapping value and MockHttpServletRequestBuilder post urlTemplate parameters as first character.
In case anyone is wondering.
If we don't use #ContextConfiguration, #WebMvcTest annotation will load the REST controller class. Otherwise, when we use use #ContextConfiguration, seems ContextConfiguration clear the context REST controller config. We need to add the REST controller to ContextConfiguration such as:
#ContextConfiguration(classes = {MockConfig.class, WeatherStationController.class})

How to dynamically load different class for JUnit tests?

I've written a couple of JUnit tests to test my REST functionality. Since I only want to test REST (and not the database, domain logic, ..) I made a stub filles with dummy data which stands for the rest of the backend.
[EDIT] For example I want to test /customers/all
A GET request will be responded to with an arraylist containing all names.
I therefore use MockMV.
this.mockMvc.perform(get("/customers/all").accept("application/json"))
.andExpect(status().isOk())
.andExpect(jsonPath("$").isNotEmpty())
.andExpect(jsonPath("$[0].name", is("John")));
When you normally perform a GET request towards /customers/all a request will be sent to the database. Now, to test my REST controller I made a stub which responds to GET /customers/all with a simple arraylist containing just my name (as you can see in the test). When I test this local I simply replace the real class with this stub. How is this done dynamically?
I don't think your approach is the good one. Just use your real controller, but stub its dependencies (using Mockito, for example), just like you would do for a traditional unit test.
Once you have an instance of the controller using stubbed dependencies, you can use a standalone setup and use MockMvc to test, in addition to the controller code, the mapping annotations, the JSON marshalling, etc.
Thias approach is described in the documentation.
Example using Mockito, assuming the controller delegates to a CustomerService:
public class CustomerControllerTest {
#Mock
private CustomerService mockCustomerService;
#InjectMocks
private CustomerController controller;
private MockMvc mockMvc;
#Before
public void setup() {
MockitoAnnotations.initMocks(this);
this.mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
}
#Test
public void shouldListCustomers() {
when(mockCustomerService.list()).thenReturn(
Arrays.asList(new Customer("John"),
new Customer("Alice")));
this.mockMvc.perform(get("/customers").accept("application/json"))
.andExpect(status().isOk())
.andExpect(jsonPath("$").isNotEmpty())
.andExpect(jsonPath("$[0].name", is("John")));
}
}

Mock external server during integration testing with Spring

I have a Spring web server that on a request makes an external call to some third-party web API (e.g. retreive Facebook oauth token). After getting data from this call it computes a response:
#RestController
public class HelloController {
#RequestMapping("/hello_to_facebook")
public String hello_to_facebook() {
// Ask facebook about something
HttpGet httpget = new HttpGet(buildURI("https", "graph.facebook.com", "/oauth/access_token"));
String response = httpClient.execute(httpget).getEntity().toString();
// .. Do something with a response
return response;
}
}
I'm writing an integration test that checks that hitting url on my server leads to some expected result. However I want to mock the external server locally so that I don't even need internet access to test all this. What is the best way to do this?
I'm a novice in spring, this is what I have so far.
#RunWith(SpringJUnit4ClassRunner.class)
#SpringApplicationConfiguration(classes = Application.class)
#WebAppConfiguration
#IntegrationTest({})
public class TestHelloControllerIT {
#Test
public void getHelloToFacebook() throws Exception {
String url = new URL("http://localhost:8080/hello_to_facebook").toString();
//Somehow setup facebook server mock ...
//FaceBookServerMock facebookMock = ...
RestTemplate template = new TestRestTemplate();
ResponseEntity<String> response = template.getForEntity(url, String.class);
assertThat(response.getBody(), equalTo("..."));
//Assert that facebook mock got called
//facebookMock.verify();
}
}
The actual real set up is more complicated - I'm making Facebook oauth login and all that logic is not in the controller but in various Spring Security objects. However I suspect that testing code is supposed to be the same since I'm just hitting urls and expect a response, isn't it?
After playing a bit with various scenarios, here is the one way how can one achieve what was asked with minimal interventions to the main code
Refactor your controller to use a parameter for thirdparty server address:
#RestController
public class HelloController {
#Value("${api_host}")
private String apiHost;
#RequestMapping("/hello_to_facebook")
public String hello_to_facebook() {
// Ask facebook about something
HttpGet httpget = new HttpGet(buildURI("http", this.apiHost, "/oauth/access_token"));
String response = httpClient.execute(httpget).getEntity().toString();
// .. Do something with a response
return response + "_PROCESSED";
}
}
'api_host' equals to 'graph.facebook.com' in application.properties in the src/main/resources
Create a new controller in the src/test/java folder that mocks the thirdparty server.
Override 'api_host' for testing to 'localhost'.
Here is the code for steps 2 and 3 in one file for brevity:
#RestController
class FacebookMockController {
#RequestMapping("/oauth/access_token")
public String oauthToken() {
return "TEST_TOKEN";
}
}
#RunWith(SpringJUnit4ClassRunner.class)
#SpringApplicationConfiguration(classes = Application.class)
#WebAppConfiguration
#IntegrationTest({"api_host=localhost",})
public class TestHelloControllerIT {
#Test
public void getHelloToFacebook() throws Exception {
String url = new URL("http://localhost:8080/hello_to_facebook").toString();
RestTemplate template = new TestRestTemplate();
ResponseEntity<String> response = template.getForEntity(url, String.class);
assertThat(response.getBody(), equalTo("TEST_TOKEN_PROCESSED"));
// Assert that facebook mock got called:
// for example add flag to mock, get the mock bean, check the flag
}
}
Is there a nicer way to do this? All feedback is appreciated!
P.S. Here are some complications I encountered putting this answer into more realistic app:
Eclipse mixes test and main configuration into classpath so you might screw up your main configuration by test classes and parameters: https://issuetracker.springsource.com/browse/STS-3882 Use gradle bootRun to avoid it
You have to open access to your mocked links in the security config if you have spring security set up. To append to a security config instead of messing with a main configuration config:
#Configuration
#Order(1)
class TestWebSecurityConfig extends WebSecurityConfig {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/oauth/access_token").permitAll();
super.configure(http);
}
}
It is not straightforward to hit https links in integration tests. I end up using TestRestTemplate with custom request factory and configured SSLConnectionSocketFactory.
If you use RestTemplate inside the HelloController you would be able to test it MockRestServiceTest, like here: https://www.baeldung.com/spring-mock-rest-template#using-spring-test
In this case
#RunWith(SpringJUnit4ClassRunner.class)
// Importand we need a working environment
#SpringBootTest(webEnvironment= SpringBootTest.WebEnvironment.RANDOM_PORT)
public class TestHelloControllerIT {
#Autowired
private RestTemplate restTemplate;
// Available by default in SpringBootTest env
#Autowired
private TestRestTemplate testRestTemplate;
#Value("${api_host}")
private String apiHost;
private MockRestServiceServer mockServer;
#Before
public void init(){
mockServer = MockRestServiceServer.createServer(this.restTemplate);
}
#Test
public void getHelloToFacebook() throws Exception {
mockServer.expect(ExpectedCount.manyTimes(),
requestTo(buildURI("http", this.apiHost, "/oauth/access_token"))))
.andExpect(method(HttpMethod.POST))
.andRespond(withStatus(HttpStatus.OK)
.contentType(MediaType.APPLICATION_JSON)
.body("{\"token\": \"TEST_TOKEN\"}")
);
// You can use relative URI thanks to TestRestTemplate
ResponseEntity<String> response = testRestTemplate.getForEntity("/hello_to_facebook", String.class);
// Do the test you need
}
}
Remember that you need a common RestTemplateConfiguration for autowiring, like this:
#Configuration
public class RestTemplateConfiguration {
/**
* A RestTemplate that compresses requests.
*
* #return RestTemplate
*/
#Bean
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
}
And that you have to use it inside HelloController as well
#RestController
public class HelloController {
#Autowired
private RestTemplate restTemplate;
#RequestMapping("/hello_to_facebook")
public String hello_to_facebook() {
String response = restTemplate.getForEntity(buildURI("https", "graph.facebook.com", "/oauth/access_token"), String.class).getBody();
// .. Do something with a response
return response;
}
}
2018 Things have improved much.
I ended up using spring-cloud-contracts
Here's a video introduction https://www.youtube.com/watch?v=JEmpIDiX7LU . The first part of the talk walk you through a legacy service. That's the one you can use for external API.
Gist is,
You create a Contract for the external service using Groovy DSL or other methods that even support explicit calls/proxy or recording. Check documentation on what works for you
Since you dont actually have control over the 3rd party in this case, you will use the contract-verifier and create the stub locally but remember to skipTests
With the stub-jar now compiled and available you can run it from within your test cases as it will run a Wiremock for you.
This question and several stackoverflow answers helped me find the solution so here is my sample project for the next person who has these and other similar microservices related tests.
https://github.com/abshkd/spring-cloud-sample-games
With everything working once you will never ever look back and do all your tests with spring-cloud-contracts
#marcin-grzejszczak the author, is also on SO and he helped a lot figure this out. so if you get stuck, just post on SO.
You could have another spring configuration file that exposes the same endpoint as the HelloController class. You could then simply return the canned json response.
From your code, I'm not sure about just what you are trying to accomplish. If you simply want to see that the call to facebook works then there's no substitute for testing against the service that actually talks to facebook. Mocking the facebook response just to ensure that it is mocked correctly, doesn't strike me as a terribly useful test.
If you are testing to see that the data that comes back from facebook is mutated in some way and you want to make sure that the work being done on it is correct, then you could do that work in a separate method that took the facebook response as a paramater, and then carried out the mutation. You could then check based on various json inputs that it was working correctly.
You could test without bringing the web service into it at all.

Resources