Florian
Florian

Reputation: 5071

How can I unit test ODataQuery and ODataQueryBuilder?

How can I unit test this code?

private ODataQueryResult buildAndExecuteQuery(String path String entity,
                                 String sapClient, String sapLanguage) {
  ODataQuery query = ODataQueryBuilder
     .withEntity(path, entity)
     .withHeader("sap-client", sapClient, true)
     .withHeader("sap-language", sapLanguage, true)
     .withoutMetadata()
     .build();
  return query.execute();
}

More precisely: How can I verify that my code calls all the right functions, for example does not forget to call withoutMetadata or set the required headers?

Unfortunately, ODataQueryBuilder and ODataQuery are classes, not interfaces. which makes mocking tricky. ODataQueryBuilder is even final, disabling mocking completely. Also, the chain starts with a static method withEntity which can also not be mocked.

Are there helpers that allow me to spy on behavior or mock data, similar to the MockUtil described in https://blogs.sap.com/2017/09/19/step-12-with-sap-s4hana-cloud-sdk-automated-testing/?

Upvotes: 0

Views: 783

Answers (2)

Florian
Florian

Reputation: 5071

Partial solution is to simulate executing the ODataQuery on a mock HttpClient.

First, the original method needs to be taken apart into two independent parts, one for building the query, the other for executing it. This is good design anyway, so no big problem:

private ODataQuery buildQuery(String path, String entity,
                              String sapClient, String sapLanguage) {
  return ODataQueryBuilder
     .withEntity(path, entity)
     .withHeader("sap-client", sapClient, true)
     .withHeader("sap-language", sapLanguage, true)
     .withoutMetadata()
     .build();
}

private ODataResponse executeQuery(ODataQuery query) {
  return query.execute();
}

The buildQuery method can now be tested as follows:

@Test
public void addsSapLanguageToHeader() throws ODataException, IOException {

    ODataQuery query = cut.buildQuery("api/v2", "business-partners", "", "fr");

    HttpUriRequest request = getRequest(query);
    assertContainsHeader(request, "sap-language", "fr");
}

The method getRequest produces a fake HttpClient that stubs all methods required to get query.execute(httpClient) to work. It stores the actual request and returns it for further inspection. Sample implementation with Mockito:

private HttpUriRequest getRequest(ODataQuery query) throws ODataException, IOException {

    // stub methods to make code work
    HttpResponse response = mock(HttpResponse.class);
    when(httpClient.execute(any())).thenReturn(response);
    StatusLine statusLine = mock(StatusLine.class);
    when(response.getStatusLine()).thenReturn(statusLine);
    HttpEntity entity = mock(HttpEntity.class);
    when(response.getEntity()).thenReturn(entity);
    InputStream inputStream = new ByteArrayInputStream("".getBytes(StandardCharsets.UTF_8));
    when(entity.getContent()).thenReturn(inputStream);
    Header[] headers = new Header[0];
    when(response.getAllHeaders()).thenReturn(headers);

    // simulate the execution of the query
    query.execute(httpClient);

    // grab the original argument from the mock for inspection
    ArgumentCaptor<HttpUriRequest> captor = ArgumentCaptor.forClass(HttpUriRequest.class);
    verify(httpClient).execute(captor.capture());
    HttpUriRequest request = captor.getValue();
    return request;
}

This solution is far from perfect, of course.

First, the amount of code needed to make this work alone shows how fragile this test will be over time. Whenever the CloudSDK decides to add a method or validation to the call sequence, this test will break. Note also that the test is invasive, by testing a private method, while gold standards say we should test only public methods.

Second, the method executeQuery can still not be tested. Execution paths also differ, because the test code uses the .execute(httpClient) variant to run the query, while the original code uses the .execute(destinationName) variant. The two happen to share code, but this may change over time.

Upvotes: 0

Christoph Schubert
Christoph Schubert

Reputation: 1124

You are right, the structure of those classes make them hard to mock, but as they are part of the "SAP Cloud Platform SDK for Service Development" we have no way to change them.

Other approaches might be:

  • If you want to stay with the Unit Test approach you might want to have a look at https://github.com/powermock/powermock. This would allow you to mock final and static classes and methods. However, I have never used it personally, so I'm not sure how easy/comfortable to use it is.
  • If you also would see an integration test suiting you could consider using http://wiremock.org/docs/getting-started/. With that you would be able to setup a "Mock Server", preparing responses for defined requests and with that verify the content of any HTTP call made by your test.

    We use WireMock in the SAP Cloud SDK and also provide some integration into our SDK via the MockUtil contained in our testutil-core module.

I hope this helps a bit!

Upvotes: 2

Related Questions