Unit Testing on Springboot WebClient with MockWebServer - spring-boot

After searching around the net for few days, couldn't really find the resources I need for my use case as many of it are implemented in Java.
I have a service class that is calling external api, and I am trying to write unit tests on it with MockWebServer
#Service
class ApiService {
#Autowired
private lateinit var webClient: WebClient
fun getApiResult(name: String): ResultDto? {
return try {
webClient
.get()
.uri("https://www.example.com/")
.retrieve()
.bodyToMono(Result::class)
.block()
catch {
null
}
}
}
#ExtendWith(MockitoExtension::class)
#MockitoSettings(strictname = Strictness.LENIENT)
#TestInstance(TestInstance.Lifecycle.PER_CLASS)
class ApiServiceTest {
#Mock
private lateinit var webClient: WebClient
#InjectMocks
private lateinit var mockApiService: ApiService
private val mockName = "mockName"
private val mockDetailDto = DetailDto(name = "mockName", age = 18)
#Test
fun canGetDetails() {
mockWebServer.enqueue(
MockResponse()
.setResponse(200)
.addHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.setBody(mockDetailDto))
)
mockApiService.getApiResult(mockName)
val request: RecordedRequest = mockWebServer.takeRequest()
// Assert statements with request
}
}
However, I am getting lateinit property webClient has not been initialised. But if I were to use #Spy instead of #Mock for the webClient in my test, it will be making actual apis calls which is not the goal of unit test. My goal would be able to check the request for some of the details such as GET and its return value. Appreciate the community's help.

By my opinion you not add some configuration, for resolve it automatically you can try make Integration Test for this. Like this
#WithMockUser//if you use spring secutity
#AutoConfigureMockMvc
#SpringBootTest(webEnvironment = RANDOM_PORT)
class MyTest {
#Autowired
private MockMvc mvc;
}
and make a test application for quickly loading environment
#SpringBootApplication
#ConfigurationPropertiesScan
#ComponentScan(lazyInit = true)//make loading is faster
public class TestApplication {
public static void main(String[] args) {
SpringApplication.run(TestApplication.class, args);
}
}
This way can load full context configuration like prod mode, of course only for called methods if you used lazyInit = true

Related

Idiomatic way of configuring integration tests in spring

What is the idiomatic (and easiest) way of configuring integration tests in Spring?
For example let's define a sample integration test:
#ActiveProfiles("dev", "test")
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class SampleTest() {
#LocalServerPort
lateinit var port: Number
lateinit var client: WebTestClient
#BeforeEach
fun setup() {
client = WebTestClient.bindToServer().baseUrl("http://localhost:$port").build()
}
#Test
fun test() {
client.get()
.uri("/api/something")
.exchange()
.expectStatus().is2xxSuccessful
.expectBody()
}
}
There are many ways i can configure WebTestClient but I don't know which one is "the best". To name a few:
lateinit var and BeforeEach
configure in each test separately
Given my project's structure and conventions most of things we get from DI is injected via constructor using #Autowired annotation. Is it also possible in this case? Therefore i'd just do:
#ActiveProfiles("dev", "test")
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class SampleTest(
#Autowired val client: WebTestClient
) {
#Test
fun test() {
client.get()
.uri("/api/something")
.exchange()
.expectStatus().is2xxSuccessful
.expectBody()
}
}
I've tried to do it without other modifications and some of my tests fail because of connection refused

Spring Boot JUnit tests fail with Status expected:<200> but was:<404>

For some time I've been struggling to make JUnit tests for my rest controller. For some reason, every time I try to run them I get the error Status expected:<200> but was:<404>. Here is my controller:
#RestController
#RequestMapping("/travels")
#RequiredArgsConstructor
public class TravelController {
private final TravelService travelService;
private final TravelOutputDtoMapper travelOutputDtoMapper;
#GetMapping
public List<TravelOutputDto> getAll() {
List<Travel> travels = travelService.getAll();
return travels.stream()
.map(travelOutputDtoMapper::travelToTravelOutputDto)
.collect(Collectors.toList());
}
}
And here is my test:
#ExtendWith(SpringExtension.class)
#WebMvcTest(controllers = TravelController.class)
#ContextConfiguration(classes = {
TravelOutputDtoMapper.class,
TravelOutputDtoMapperImpl.class
})
class TravelControllerTest {
#Autowired
private MockMvc mockMvc;
#MockBean
private TravelService travelService;
#Autowired
private TravelOutputDtoMapper travelOutputDtoMapper;
#Test
void testGetAll() throws Exception {
List<Travel> travels = mockTravelList();
Mockito.when(travelService.getAll()).thenReturn(travels);
mockMvc.perform(get("/travels"))
.andExpect(status().isOk());
}
private List<Travel> mockTravelList() {
// Dummy travel list
}
}
I think the reason is connected with TravelOutputDtoMapper as if I remove it from the controller and don't try to inject it the tests are passing, but I cannot find any information why it is doing it. The autowired mapper has an instance and works just fine.
Here is the Mapper:
#Mapper(componentModel = "spring")
public interface TravelOutputDtoMapper {
#Mapping(target = "from", source = "entity.from.code")
#Mapping(target = "to", source = "entity.to.code")
TravelOutputDto travelToTravelOutputDto(Travel entity);
}
The #ContextConfiguration annotation is used for a different purpose:
#ContextConfiguration defines class-level metadata that is used to determine how to load and configure an ApplicationContext for integration tests.
Using Spring Boot and #WebMvcTest there's no need to manually specify how to load the context. That's done for you in the background.
If you'd use this annotation, you'd specify your main Spring Boot class here (your entry-point class with the #SpringBootApplication annotation).
From what I can see in your test and your question is that you want to provide an actual bean for the TravelOutputDtoMapper, but mock the TravelService.
In this case, you can use #TestConfiguration to add further beans to your sliced Spring TestContext:
// #ExtendWith(SpringExtension.class) can be removed. This extension is already registered with #WebMvcTest
#WebMvcTest(controllers = TravelController.class)
class TravelControllerTest {
#Autowired
private MockMvc mockMvc;
#MockBean
private TravelService travelService;
#Autowired
private TravelOutputDtoMapper travelOutputDtoMapper;
#TestConfiguration
static class TestConfig {
#Bean
public TravelOutputDtoMapper travelOutputDtoMapper() {
return new TravelOutputDtoMapper(); // I assume your mapper has no collaborators
}
}
// ... your MockMvc tests
}

Spring Data Rest cannot do Integration test?

I have tried to use both MockMVC and TestRestTemplate. In both cases, the response back is 404 but the API endpoints work outside of integration test (when I run the spring app on its own).
Does anyone have a working sample app that has a working integration test for a generated controller using Spring Data Rest?
I was also able to write regular integration tests against my own controllers (Non SDR types)
Test code:
#ExtendWith(SpringExtension.class)
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class MyTest {
#Autowired
private TestRestTemplate testRestTemplate;
#Test
public void testApi() {
String settings = testRestTemplate
.getForObject("/api/v1/orders", String.class);
System.out.println(settings);
}
}
Repo:
#RepositoryRestResource(excerptProjection = OrderSummaryProjection.class)
public interface OrderRepository extends JpaRepository<Order, Long> {}
Ok I found out the issue but I dont know what the answer should be:
I set spring.data.rest.basePath in application.properties.
But I don't think that file is read when you run the integration tests. How do I fix that?
I currently don't test Spring Data Rest endpoints, but if I were to do it, I would test interfaces using classical Integration test approach:
#RunWith(SpringRunner.class)
#SpringBootTest
public class DummyIT {
#Autowired
private SettingsRepository settingsRepository;
#Test
public void testApi() {
List<Settings> settings = settingsRepository.findAll();
assertNotNull(settings);
}
}
I also tested end-to-end test and it also works, it just returns ugly {"_embedded" : {"settings" : [ { ... } ] }, ... } so it's doable, but it's not pretty:
#RunWith(SpringRunner.class)
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class DummyTest {
#Autowired
private TestRestTemplate testRestTemplate;
#Test
public void testApi() {
String settings = testRestTemplate
.getForObject("/api/settings", String.class);
System.out.println(settings);
}
}

mock rest template for unit test

I want to mock a RestTemplate in Spring Boot, where I'm making a REST call within a method. To test the controller of the microservice I'm creating,
I want to test methods inside the controller of my micro service.
For example:
#GetMapping(value = "/getMasterDataView", produces = { MediaType.APPLICATION_JSON_VALUE })
#CrossOrigin(origins = { "http://192.1**********" }, maxAge = 3000)
public ResponseEntity<MasterDataViewDTO> getMasterDataView() throws IOException {
final String uri = "http://localhost:8089/*********";
RestTemplate restTemplate = new RestTemplate();
MasterDataViewDTO masterDataViewDTO = restTemplate.getForObject(uri, MasterDataViewDTO.class);
return new ResponseEntity<>(masterDataViewDTO, HttpStatus.OK);
}
how to I test this using mocking?
This is what I have so far:
#Test
public void testgetMasterDataView() throws IOException {
MasterDataViewDTO masterDataViewDTO= mock(MasterDataViewDTO.class);
//String uri = "http://localhost:8089/*********";
Mockito.when(restTemplate.getForObject(Mockito.anyString(),ArgumentMatchers.any(Class.class))).thenReturn(masterDataViewDTO);
assertEquals("OK",inquiryController.getMasterDataView().getStatusCode());
}
I am getting an error when I'm running the mock, the method getMasterDataView() is getting called and the REST call within it is also getting called and is throwing an error. How can I write my test so that the REST endpoint is not called? If it's possible, I'd like to do this with Mockito.
Before you start writing a test, you should change your code a bit. First of all, it would be a lot easier if you extracted that RestTemplate, and created a separate bean for it which you would inject within your controller.
To do that, add something like this within a #Configuration class or within your main class:
#Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
Additionally, you have to remove the new RestTemplate() from your controller, and autowire it in stead, for example:
#Autowired
private RestTemplate restTemplate;
Now that you've done that, it's going to be a lot easier to inject a mock RestTemplate within your tests.
For your testing, you have two options:
Either mock RestTemplate and all the methods you are trying to access, using a mocking framework (eg. Mockito)
Or you can use MockRestServiceServer, which allows you to write tests that verify if the URLs are properly called, the request matches, and so on.
Testing with Mockito
To mock your RestTemplate with Mockito, you have to make sure that you add the following annotation to your tests:
#RunWith(MockitoJUnitRunner.class)
After that, you can do this:
#InjectMocks
private MyController controller;
#Mock
private RestTemplate restTemplate;
And now you can adjust your tests like this:
#Test
public void testgetMasterDataView() throws IOException {
MasterDataViewDTO dto = new MasterDataViewDTO();
when(restTemplate.getForObject("http://localhost:8089/*********", MasterDataViewDTO.class)).thenReturn(dto);
ResponseEntity<MasterDataViewDTO> response = controller.getMasterDataView();
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).isEqualTo(dto);
}
You could mock the DTO as you did within your test, but you don't have to, and I don't think there's any benefit from doing so. What you do have to mock is the restTemplate.getForObject(..) call.
Testing with MockRestServiceServer
Another approach is to use MockRestServiceServer. To do that, you have to use the following annotations for your test:
#RunWith(SpringRunner.class)
#RestClientTest
And then you'll have to autowire your controller and MockRestServiceServer, for example:
#Autowired
private MyController controller;
#Autowired
private MockRestServiceServer server;
And now you can write tests like this:
#Test
public void testgetMasterDataView() throws IOException {
server
.expect(once(), requestTo("http://localhost:8089/*********"))
.andExpect(method(HttpMethod.GET))
.andRespond(withSuccess(new ClassPathResource("my-mocked-result.json"), MediaType.APPLICATION_JSON));
ResponseEntity<MasterDataViewDTO> response = controller.getMasterDataView();
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
// TODO: Write assertions to see if the DTO matches the JSON structure
}
In addition to testing that your actual REST call matches, this also allows you to test if your JSON-to-DTO works as well.
You can achieve this by using #RestClientTest and MockRestServiceServer. An example provided in their documentation:
#RunWith(SpringRunner.class)
#RestClientTest(RemoteVehicleDetailsService.class)
public class ExampleRestClientTest {
#Autowired
private RemoteVehicleDetailsService service;
#Autowired
private MockRestServiceServer server;
#Test
public void getVehicleDetailsWhenResultIsSuccessShouldReturnDetails()
throws Exception {
this.server.expect(requestTo("/greet/details"))
.andRespond(withSuccess("hello", MediaType.TEXT_PLAIN));
String greeting = this.service.callRestService();
assertThat(greeting).isEqualTo("hello");
}
}
create bean instead of using new RestTemplate() in your services.
use Profile("!test") for your bean that use only for non test profile.
create test class like this:
#SpringBootTest(properties = "spring.profiles.active:test")
#ActiveProfiles("test")
#RunWith(SpringRunner.class)
#Log4j2
#Transactional
public class QabzinoMockTest {
#Autowired
private WebApplicationContext webApplicationContext;
#Autowired
private QabzinoLogRepository qabzinoLogRepository;
private MockMvc mockMvc;
private final WebServiceStatus successStatus = new WebServiceStatus();
#Mock
private RestTemplate restTemplate;
private final Gson mapper = new Gson();
#TestConfiguration
static class Config {
#Bean
public RestTemplate rest() {
return Mockito.mock(RestTemplate.class);
}
}
#Before
public void setup() throws Exception {
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.webApplicationContext).build();
this.restTemplate = (RestTemplate) this.webApplicationContext.getBean("rest");
LoginOutput responseModel = new LoginOutput();
responseModel.setStatus("200");
Mockito.when(
restTemplate.postForEntity(
Mockito.eq(LOGIN_URL),
Mockito.isA(HttpEntity.class),
Mockito.eq(String.class)
)
).thenReturn(ResponseEntity.ok().body(mapper.toJson(responseModel)));
}
}
in this sample we create bean with rest name in static class Config and mock it in setup method Before all tests.
and preferred test config maybe useful for you application-test.properties:
spring.h2.console.enabled=true
spring.jpa.defer-datasource-initialization=true
spring.jpa.show-sql=true
spring.jpa.generate-ddl=true
spring.jpa.hibernate.ddl-auto=create
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=TRUE;IGNORECASE=TRUE;
spring.datasource.username=sa
spring.datasource.password=sa
and finally when we see restTemplate in our code, mock bean return our responseModel instead of real service call :)

Spring boot 2.0.5.RELEASE - sleuth and mockito

I have tried to sort this out for a week, but no luck at all. The issue is with the unit tests.
This is the class that I am trying to test:
import brave.Span;
import brave.Tracer;
#Service
public class InternetBackEndRestClient {
#Autowired
private Tracer tracer;
public PasswordJwtResponse generatePassworJwt(PasswordJwtRequest passwordJwtRequest, String traceId) throws LogonProxyException {
log.info("{\"Starting method\": \"generatePassworJwt\", \"input\": {} }", passwordJwtRequest);
Span newSpan = tracer.nextSpan().name("spanPasswordJwtResponse");
...
}
}
How can I do the unit test? Brave.Tracer is a final class so that I cannot mock it. Is there anyway to set up a context? or mock Tracer?
#RunWith(MockitoJUnitRunner.class)
public class InternetBackEndRestClientTest {
#InjectMocks
private InternetBackEndRestClient internetBackEndRestClient;
#Mock
private Tracer tracer;
#Test
public void generatePassworJwt_test() {
internetBackEndRestClient.generatePassworJwt(...);
....
}
}
Could anyone help me please?
Here is the solution that worked for me:
#RunWith(MockitoJUnitRunner.class)
public class InternetBackEndRestClientTest {
private static final String TRACEID = "12345678901234567890123456789012";
#InjectMocks
private InternetBackEndRestClient internetBackEndRestClient;
#Mock
private Tracer tracer;
#Mock
private Span span;
#Before
public void setUp() {
MockitoAnnotations.initMocks(this);
when(tracer.nextSpan()).thenReturn(span);
when(tracer.nextSpan().name("spanPasswordJwtResponse"))
.thenReturn(span);
when(span.start()).thenReturn(span);
Tracing tracing = Tracing.newBuilder().build();
doReturn(tracing.tracer().withSpanInScope(span))
.when(tracer).withSpanInScope(span);
doNothing().when(span).finish();
...
}
...
}
You can manually set the span and trace id using TraceContext.newBuilder() in a test and past the Tracer into the class being tested.
Tracer tracer = Tracing.newBuilder().build().tracer();
TraceContext ctx = TraceContext.newBuilder().traceId(10L).spanId(10L).build();
Span span = tracer.toSpan(ctx);
tracer.withSpanInScope(span);
This might be a bit lighter than mocking the Tracer class
Your example isn't complete so it's hard to identify everything that's not quite right, but one thing is that #MockBean will only work if you're using Spring Boot testing's infrastructure. That means that you need to be using SpringRunner to run the test and you also have to have enabled #MockBean support. The most common way to do that is with #SpringBootTest:
#SpringBootTest
#RunWith(SpringRunner.class)
public class InternetBackEndRestClientTest {
// …
}
You can read more about #MockBean in the Spring Boot reference documentation.

Resources