Home > Net >  Spring Boot Test : Mix up @MockBean and @SpyBean in same Test class on same Bean object?
Spring Boot Test : Mix up @MockBean and @SpyBean in same Test class on same Bean object?

Time:12-15

Here a test code sample with Spring Boot Test:

@SpringBootTest
@AutoConfigureMockMvc
class SendMoneyControllerSpringBootSpyBeanTest {

  @Autowired
  private MockMvc mockMvc;

  @SpyBean
  private SendMoneyUseCase sendMoneyUseCase;

  @Test
  void testSendMoneyWithMock() {
    ...
  }

  @Test
  void testSendMoneyWithSpy() {
    ...
  }

}

Now suppose the two test methods like in the snippet above. One is using the spy version of the spring bean, whereas the other is using a mock version of the spring bean. How can I mix up both and distinguish them in my test methods ? For example, can I do :

  @SpringBootTest
@AutoConfigureMockMvc
class SendMoneyControllerSpringBootSpyBeanTest {

  @Autowired
  private MockMvc mockMvc;

  @MockBean
  private SendMoneyUseCase sendMoneyUseCaseMocked;

  @SpyBean
  private SendMoneyUseCase sendMoneyUseCaseSpied;
}

I know that in spring bean container, there is only one of sendMoneyUseCaseMocked or sendMoneyUseCaseSpied because they are the same java type. I can use a qualifier, to refer them by name. But in that case, how do I write my mock condition in the corresponding test method (write either mock condition on the mocked bean or the spied condition on the spied bean in the concerned test method).

EDIT : Another approach is to remove the line of code @MockBean, like this, the spied method is working. Consequently, I need then to programmatically code @MockBean in the mocked test method with Spring boot API, but how to do ?.

Thx.

CodePudding user response:

I see two solutions.

  1. Either use two test classes. One with the @MockBean object and one with the @SpyBean object. Both can inherit from the same abstract parent-class.

  2. Let Spring Boot inject @MockBean and then replace it manually in the testSendMoneyWithSpy() test with the @SpyBean object using reflection.

But, why do you want to use the spy-object at all? If you make unit-tests, you should mock the service and only test the controller here. And test the SendMoneyUseCase class in the SendMondeyUseCaseTest test.

(btw. strange name SendMondeyUseCase why not SendMoneyService or SendMoneyComponent. Or MoneySendService?)

CodePudding user response:

Here the solution, first the solution code (my real test case, the one in first topic is more like a study code) :

@Slf4j
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class DocumentsApiTest extends ProductAPITestRoot {

    @Autowired
    private TestRestTemplate restTemplate;

    @SpyBean
    private AlfrescoService alfrescoServiceSpy;

    @MockBean
    private CloseableHttpClient closeableHttpClient;

    @Test
    public void testUploadDocument_SUCCESS() {
        HttpHeaders headers = createOKHttpHeaders();
        DocumentWithFile documentWithFile = createDocumentWithFile();

        doReturn(StringUtils.EMPTY).when(alfrescoServiceSpy).sendDocument(anyString(), any(InputStream.class), eq(documentWithFile.getNomFichier()));
        HttpEntity<DocumentWithFile> request = new HttpEntity<>(documentWithFile, headers);

        ResponseEntity<Long> response = restTemplate.exchange(
                "/documents/upload", HttpMethod.POST, request, Long.class);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(response.getBody()).isEqualTo(1L);

    }

    @Test
    public void testUploadDocument_500_INTERNAL_SERVER_ERROR() {
        HttpHeaders headers = createOKHttpHeaders();
        DocumentWithFile documentWithFile = createDocumentWithFile();


        doThrow(new AlfrescoServiceException("Alfresco has failed !")).when(alfrescoServiceSpy).sendDocument(anyString(), any(InputStream.class), eq(documentWithFile.getNomFichier()));
        HttpEntity<DocumentWithFile> request = new HttpEntity<>(documentWithFile, headers);

        ResponseEntity<String> response = restTemplate.exchange(
                "/documents/upload", HttpMethod.POST, request, String.class);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR);
        assertThat(response.getBody()).contains("Internal Server Error");
    }

    @Test
    public void testUploadDocument_502_BAD_GATEWAY() throws IOException {
        HttpHeaders headers = createOKHttpHeaders();
        DocumentWithFile documentWithFile = createDocumentWithFile();

        CloseableHttpResponse alfrescoResponse = mock(CloseableHttpResponse.class);
        when(closeableHttpClient.execute(any(HttpPost.class))).thenReturn(alfrescoResponse);
        when(alfrescoResponse.getStatusLine()).thenReturn(new BasicStatusLine(HttpVersion.HTTP_1_1, HttpStatus.BAD_GATEWAY.value(), "FINE!"));

        HttpEntity<DocumentWithFile> request = new HttpEntity<>(documentWithFile, headers);

        ResponseEntity<String> response = restTemplate.exchange(
                "/documents/upload", HttpMethod.POST, request, String.class);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_GATEWAY);
        assertThat(response.getBody()).contains("Erreur Alfresco");
    }


    private static HttpHeaders createOKHttpHeaders() {
        HttpHeaders headers = new HttpHeaders();
        headers.set("Authorization", "Bearer "   OK_TOKEN);
        headers.setContentType(MediaType.APPLICATION_JSON);
        return headers;
    }

    private static DocumentWithFile createDocumentWithFile() {
        String fileAsString = RandomStringUtils.randomAlphabetic((int) 1e6);
        byte[] fileAsStringBase64 = Base64.getEncoder().encode(fileAsString.getBytes());
        DocumentWithFile documentWithFile = new DocumentWithFile();
        String nomFichierExpected = "nomFichier.pdf";
        documentWithFile
                .id(8L)
                .idCatalogue("idCatalogue")
                .nom("nom")
                .reference("reference")
                .version("version")
                .type(TypeDocument.IPID)
                .fichier(new String(fileAsStringBase64))
                .nomFichier(nomFichierExpected);
        return documentWithFile;
    }
}

If you look carrefully, the right way to mixup both : a mock and a spy of the same bean.

  1. You can use @MockBean....and you use the spy version of this @MockBean with when(...).thenCallRealMethod(). But the REAL drawback of this is that if the @MockBean bean contains @Value field injection, thus they are NOT initialized. Meaning that @MockBean annotation set @Value fields of the mocked bean to null. So I went for solution 2) because I need injection of @Value fileds.

  2. Instead of @MockBean use @SpyBean of the concerned spring bean. Like this, you've got now the real bean. The question is how do I use it like a @MockBean. So to use a @SpyBean like a @MockBean, you needs to force the returned value of your @SpyBean bean like this for example :

    doReturn(StringUtils.EMPTY).when(alfrescoServiceSpy).sendDocument(anyString(), any(InputStream.class), eq(documentWithFile.getNomFichier()));

As you can see, although alfrescoServiceSpy call the real code (not a mock), then you still can change its default behavior (calling the real code) with the mocked behavior like the snipped code above as example.

So test methods that need the mock version of @SpyBean declare an instruction of mocked behavior to do. And test methods that needs real code they don't do anything and @Value will be injected into the bean annotated @SpyBean the right way.

I'am personnaly satisfied of this code version.

Thanks you very much all.

  • Related