Technology Blogs by SAP
Learn how to extend and personalize SAP applications. Follow the SAP technology blog for insights into SAP BTP, ABAP, SAP Analytics Cloud, SAP HANA, and more.
cancel
Showing results for 
Search instead for 
Did you mean: 
philipp_herzig
Employee
Employee
The following steps will explain how you can mock SAP S/4HANA-specific dependencies inside your tests using the SAP S/4HANA Cloud SDK.

Note: This post is part of a series. For a complete overview visit the SAP Cloud SDK Overview.


Goal of this blog post


This blog post introduces to you the mocking capabilities of the SAP Cloud SDK in more detail. After this blog you will be able to understand

  • What mocking is and why it is a useful tool and programming capability.

  • How you can mock the access to SAP S/4HANA systems enabling you to test and develop your S/4HANA extension without an S/4HANA system.


Prerequisites


In order to successfully go through this tutorial you have to complete the tutorial at least until:

As we will also refer to mocking of resilient S/4HANA calls, it is beneficial to also be familiar with resilient programming as outlined in:

In addition, we will use the virtual data model for OData as introduced in

In addition, complete mocking of S/4HANA APIs only works stable as of SAP Cloud SDK version 1.6.0. Therefore, please make sure, your SDK Bill of Material is updated accordingly like shown below:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.sap.cloud.s4hana</groupId>
<artifactId>sdk-bom</artifactId>
<version>1.6.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
<!-- possibly further managed dependencies ... -->
</dependencyManagement>

 

Mocking Theory


Motivation


In our previous tutorials and deep dives such as

we have been mostly using so-called integration tests to test the functionality of our application in conjunction with the SAP S/4HANA system. While this provides a very convenient way to test the overall functionality from SCP backend to S/4HANA backend, having too many of them leads to brittle and hard to maintain test suites.

First, you actually require a SAP S/4HANA system which is configured and up-and-running to provide the APIs you require. This means you have to provide and maintain a corresponding S/4HANA system during development to run your tests. This can be expensive and potentially error-prone leading to longer feedback cycles for the software engineering team. In addition, sometimes you may require updates to the S/4HANA backend system which are not yet transported into the system instance you are using for running your integration tests. This slows down feedback cycles even further and potentially stalls effective progress with your SCP extension leading to frustration of the software development team.

Secondly, in order to fulfill comprehensive testing, code coverage and other quality requirements it is all too tempting providing everything as integration tests. However, integration tests are slow, in particular when you have many of them. They require to bootstrap a full or embedded container (e.g., TomEE as provided with the SAP Cloud SDK) as well as a complete, functional connection to S/4HANA doing real RESTful calls over the wire. Hence, the more your application grows, the slower your test suite eventually becomes. This has a negative impact on your development team performance, is frustrating and may require additional significant investments into vertical or horizontal scaling.

Therefore, also as the testing pyramid below suggests, you shall have much more unit tests in your codebase than integration tests. Unit tests are self-contained and only test a small portion (e.g., class) of the codebase within one test. Therefore, issues can be detected faster and unit tests run much faster (usually one to two orders of magnitude) than integration tests. Additionally, unit tests force application developers to write more testable application code and apply separation of concerns more thoroughly which usually leads to better maintainable and understandable code.



However, this yields the question how you can test a small portion of your codebase, if it is directly or indirectly dependent on some integration point with SAP S/4HANA, a real database or simply another Java object that is not part of the testing scope.

The answer is simple: Use Mocking.

What is Mocking


Mocking in general refers to the idea of providing test doubles instead of the real implementation during a test. Like in movies, a double of an actress comes into play when particular risky scenes such as stunts are recorded which cannot be done by the Oscar-nominated movie star. With testing, we are not so much concerned about the well-being of the double but rather its runtime costs and state which might be hard to provide or imitate during testing.

When writing tests we are oftentimes faced with the situation that it is too cumbersome or sometimes even impossible to instantiate the entire program or produce a certain state of the program. In this case, we can provide test doubles that act on behalf of the real implementation. Test doubles comes in different forms such as mocks, stubs, spies, fake, or dummy objects. We leave the subtle differences to the plenty of related work such as Martin Fowler's - Mocks Aren't Stubs or lengthy Stackoverflow debates.

In the following text, we will mostly refer to mock or stub objects when mocking behavior and state respectively.

Mocking the Business Partner API


In the following tutorial, we will introduce three basic mocking principles that should empower you with all the tools and requirements you need. We will go over the following important aspects:

  • Mocking the happy path generically: Return successfully all business partners without an S/4HANA system.

  • Mocking specific S/4HANA API calls: Return successfully specific business partners that match certain criteria.

  • Mocking the failure path: Return an error thrown by the business partner API without an S/4HANA system.


Mocking the happy path


Let's start with a very simple example of a GetBusinessPartnerCommand which retrieves all business partners' first and last names that are customers. If you did all the tutorials at least until Step 5 with SAP Cloud SDK: Resilience with Hystrix plus Step 10 with SAP Cloud SDK: Virtual Data Model for OData, the code should be very familiar to you.

The file needs to be put under your <projectroot>/application/src/main/java directory.
public class GetBusinessPartnerCommand extends ErpCommand<List<BusinessPartner>> {

private BusinessPartnerService businessPartnerService;

public GetBusinessPartnerCommand(ErpConfigContext erpConfigContext, BusinessPartnerService businessPartnerService) {
super(GetBusinessPartnerCommand.class, erpConfigContext);
this.businessPartnerService = businessPartnerService;
}

@Override
protected List<BusinessPartner> run() {

try {

return businessPartnerService.getAllBusinessPartner()
.filter(BusinessPartner.IS_NATURAL_PERSON.eq("X"))
.select(BusinessPartner.FIRST_NAME,
BusinessPartner.LAST_NAME)
.execute(getConfigContext());

} catch (final ODataException e) {
throw new HystrixBadRequestException(e.getMessage(), e);
}
}
}



________________________

What the code does

The code above defines a new class called GetBusinessPartnerCommand which inherits from ErpCommand, thus, is resilient to network failures and latency issues to S/4HANA. The actual logic is executed within the run() method where we are doing a type-safe OData Call to the BusinessPartner API of SAP S/4HANA, filtering on the isNaturalPerson property of the business partner and doing a partial projection on the firstname and lastname properties.
________________________


Unlike previous tutorials, we do not want to test this now against a real S/4HANA system but just by a locally running test.

For this, we put the following test file called GetBusinessPartnerMockedTest under <projectroot>/unit-tests/src/test/java.
import com.sap.cloud.sdk.odatav2.connectivity.ODataException;
import com.google.common.collect.Lists;
import com.sap.cloud.sdk.testutil.MockUtil;
import org.mockito.junit.MockitoJUnitRunner;

import static org.junit.Assert.*;
import static org.mockito.Mockito.*;

public class GetBusinessPartnerMockedTest
{
private static final MockUtil mockUtil = new MockUtil();
private static BusinessPartner alice;
private static BusinessPartner bob;

@BeforeClass
public static void beforeClass() throws Exception {
mockUtil.mockDefaults();
mockUtil.mockDestination("ErpQueryEndpoint", URI.create(""));

alice = new BusinessPartner();
alice.setFirstName("Alice");

bob = new BusinessPartner();
bob.setFirstName("Bob");
}

@Test
public void testGetAnyBusinessPartner() throws Exception
{
final BusinessPartnerService service = Mockito.mock(BusinessPartnerService.class, RETURNS_DEEP_STUBS);

when(service.getAllBusinessPartner()
.filter(any(ExpressionFluentHelper.class))
.select(any(EntityField.class))
.execute(any(ErpConfigContext.class)))
.thenReturn(Lists.newArrayList(alice, bob));

final List<BusinessPartner> businessPartnerList = new GetBusinessPartnerCommand(new ErpConfigContext(), service).execute();

assertEquals(2, businessPartnerList.size());
assertEquals("Alice", businessPartnerList.get(0).getFirstName());
assertEquals("Bob", businessPartnerList.get(1).getFirstName());
}
}


________________________



Understanding the code

The code above creates a new test class called GetBusinessPartnerMockedTest. The code is structured in three main parts.

First, the initialization of the mocking facility MockUtil provided by the SAP Cloud SDK as well as the declaration of two test business partners, Alice and Bob.

Secondly, an initialization method beforeClass() which is annotated with @BeforeClass meaning that this method is called exactly once before the execution of all other test methods. Inside the method, all the required SAP Cloud Platform mocks are initialized (tenant, user, etc). In addition, we mock a dummy S/4HANA destination. Finally, this method initializes two test business partners with their intended firstnames.

Thirdly, our first actual testing method testGetAnyBusinessPartner().

Here, we first of all create a mock (i.e., a proxy object) of the business partner service interface that replaces the actual SDK implementation with mocked methods. As we want to mock the entire fluent API including all its delegating classes and not just the interface itself, we also need to provide the RETURNS_DEEP_STUBS option.

Then, we do the actually mocking by defining under which condition (when-part), what should happen (then-part). Here, we simply define that when the business partner APIs is called, upon any filter condition, upon any selection criteria, upon any execution context, it shall return Alice and Bob as business partners.

Afterwards, we initialize our GetBusinessPartnerCommand and pass the mocked business partner service into our production code command.

Next, we simply call the command as we do in our production code to get the list of business partners who match our criteria.
________________________

 


You can try it out by right-clicking on your test class and select "Run As Unit Test" or execute mvn clean test on your root project.

If you use an IDE you should see the test passing in very quick time.



Also, in any IDE we can look at the code coverage that this test provides. So far, the test accounts for 77% of all lines of code (we will achieve 100% in this tutorial).


Mocking more specific S/4HANA calls.


In the example above, we have simply mocked out any selection and projection criteria using any(ExpressionFluentHelper.class) or any(BusinessPartnerSelectable.class). Hence, our mock is not very specific to the actual production code where we filter only for business partners who are customers.


Suppose Alice is a customer and Bob is a supplier. So the question is whether we can mock more specifically to match the actual API semantics.


For this purpose, we write another test method called testGetSpecificBusinessPartner:



@Test
public void testGetSpecificBusinessPartner() throws Exception
{
final BusinessPartnerService service = Mockito.mock(BusinessPartnerService.class, RETURNS_DEEP_STUBS);

when(service.getAllBusinessPartner()
.filter(BusinessPartner.IS_NATURAL_PERSON.eq("X"))
.select(any(BusinessPartnerSelectable.class))
.execute(any(ErpConfigContext.class)))
.thenReturn(Lists.newArrayList(alice));

final List<BusinessPartner> businessPartnerList = new GetBusinessPartnerCommand(new ErpConfigContext(), service).execute();

assertEquals(1, businessPartnerList.size());
assertEquals("Alice", businessPartnerList.get(0).getFirstName());
}

Here, we have now replaced the line .filter(any(ExpressionFluentHelper.class)) with  .filter(BusinessPartner.CUSTOMER.ne("")) that matches our production code of GetBusinessPartnerCommand. Furthermore, we only return Alice in this case and adapted the test assertions accordingly.









Homework

While this tests runs successfully, we invite you to play around with your production or test code now. For example, if you change your application code and change the filter condition to something else, your test suite is now able to spot the potential bug of changed semantics, even though there is no S/4HANA system connected to prove this new semantics.


 

Mocking the failure case


Mocking is also a very powerful technique to test hard-to-reach code such as certain failure cases.

In our example, we can run into situations where the OData call to the business partner API may fail during runtime (due to failing network, wrong authorizations, etc.). This is unfortunate and we cannot do anything about failing OData calls at design-time, but we must test whether our code behaves correctly in such failure situations and leaves the application in an expected, consistent state.

Fortunately, we can also mock such failure cases by throwing a dummy ODataException and expect a HystrixBadRequestException as the test result (in case you have not stumbled over the HystrixBadRequestException yet, we just mention that this is the standard contract of Hystrix to signal an expected exception that must not trigger any resilience functionality such as circuit breakers).
@Test(expected = HystrixBadRequestException.class)
public void testGetBusinessPartnerFailure() throws Exception {
final BusinessPartnerService service = Mockito.mock(BusinessPartnerService.class, RETURNS_DEEP_STUBS);

when(service.getAllBusinessPartner()
.filter(any(ExpressionFluentHelper.class))
.select(any(BusinessPartnerSelectable.class))
.execute(any(ErpConfigContext.class)))
.thenThrow(new ODataException(ODataExceptionType.METADATA_FETCH_FAILED, "Something went wrong", null));

new GetBusinessPartnerCommand(new ErpConfigContext(), service).execute();
}

When we run all our three test cases together, we finally end up with a 100% tested codebase, although we do not have used any real S/4HANA system.



 

Recommendation and Hints


The mocking facilities of the SAP Cloud SDK are based on the open source framework Mockito. Therefore, we recommend you to study the official documentation for more options and capabilities.

  1. One of the additional possibilities is to use the @Mock annotations for providing the mocks in the tests which lead to less code and more readable tests. For example, we can mock the business partner service as well as our two business partner, Alice and Bob and can leave out the mocking per test method. Please note that the JUnit test has to be annotated with @RunWith(MockitoJUnitRunner.Silent.class).The full running code for this is here:
    import com.sap.cloud.sdk.odatav2.connectivity.ODataException;
    import com.google.common.collect.Lists;
    import com.sap.cloud.sdk.testutil.MockUtil;
    import org.mockito.junit.MockitoJUnitRunner;

    import static org.junit.Assert.*;
    import static org.mockito.Mockito.*;

    @RunWith(MockitoJUnitRunner.Silent.class)
    public class GetBusinessPartnerMockedTest
    {
    private static final MockUtil mockUtil = new MockUtil();

    @Mock(answer = Answers.RETURNS_DEEP_STUBS)
    private BusinessPartnerService service;

    @Mock
    private BusinessPartner alice;

    @Mock
    private BusinessPartner bob;

    @Before
    public void before() {
    mockUtil.mockDefaults();
    mockUtil.mockDestination("ErpQueryEndpoint", URI.create(""));

    when(alice.getFirstName()).thenReturn("Alice");
    when(bob.getFirstName()).thenReturn("Bob");
    }

    @Test
    public void testGetAnyBusinessPartner() throws Exception
    {
    when(service.getAllBusinessPartner()
    .filter(any(ExpressionFluentHelper.class))
    .select(any(BusinessPartnerSelectable.class))
    .execute(any(ErpConfigContext.class)))
    .thenReturn(Lists.newArrayList(alice, bob));

    final List<BusinessPartner> businessPartnerList = new GetBusinessPartnerCommand(new ErpConfigContext(), service).execute();

    assertEquals(2, businessPartnerList.size());
    assertEquals("Alice", businessPartnerList.get(0).getFirstName());
    assertEquals("Bob", businessPartnerList.get(1).getFirstName());
    }

    @Test
    public void testGetSpecificBusinessPartner() throws Exception
    {
    final BusinessPartnerService service = Mockito.mock(BusinessPartnerService.class, RETURNS_DEEP_STUBS);

    when(service.getAllBusinessPartner()
    .filter(BusinessPartner.IS_NATURAL_PERSON.eq("X"))
    .select(any(BusinessPartnerSelectable.class))
    .execute(any(ErpConfigContext.class)))
    .thenReturn(Lists.newArrayList(alice));

    final List<BusinessPartner> businessPartnerList = new GetBusinessPartnerCommand(new ErpConfigContext(), service).execute();

    assertEquals(1, businessPartnerList.size());
    assertEquals("Alice", businessPartnerList.get(0).getFirstName());
    }

    @Test(expected = HystrixBadRequestException.class)
    public void testGetBusinessPartnerFailure() throws Exception {

    when(service.getAllBusinessPartner()
    .filter(any(ExpressionFluentHelper.class))
    .select(any(BusinessPartnerSelectable.class))
    .execute(any(ErpConfigContext.class)))
    .thenThrow(new ODataException(ODataExceptionType.METADATA_FETCH_FAILED, "Something went wrong", null));

    new GetBusinessPartnerCommand(new ErpConfigContext(), service).execute();
    }
    }


  2. The test itself can be still refactored to avoid duplicate code. For example, certain mock conditions, e.g., any business partner API call could be externalized into a separate method for better reuse and readability.

  3. The mocking facility also works greatly with the SAP Cloud SDK Continuous Delivery Pipeline as introduced in Step 14 with SAP Cloud SDK: Continuous integration and delivery. This helps to achieve higher code coverage and higher qualities which are checked by the pipeline.


Unit tests are not the ultimate answer to everything. They do not replace integration or system tests which test the emergent behavior of the entire system. This raises the question when to provide a unit and when to provide an integration test.

As explained above, you should give favor to unit tests whenever possible to ensure that your test suite executes fast. However, integration tests are a nice way to test the overall business semantics of your application, in particular, when multiple, complex interactions should be tested under "more real" conditions ("more real" means that no matter how much effort you put into testing, you will never get as real as the productive system, in particular, in highly distributed cloud applications).

In addition, for mocking the S/4HANA APIs as explained above, you have to have a certain understanding of how the APIs behave, otherwise it would be hard to assert their behaviors. When you do not have any S/4HANA system yet, we recommend you to use the API Business Hub Sandbox to understand the behavior to make better mocking assumptions.

Summary


In this tutorial, we have shown how you can leverage the SAP Cloud SDK to mock S/4HANA calls during development to create fast running unit tests which require a data dependency to SAP S/4HANA. This way, you can start building and testing your S/4HANA extension on SCP already without having an SAP S/4HANA system. When you utilize an S/4HANA system for integration tests, you can make sure that you have much more unit tests than integration tests to maintain a fast, non-brittle test suite.

Questions?


Reach out to us on Stackoverflow via our s4sdk tag. We actively monitor this tag in our core engineering teams.

Alternatively, we are happy to receive your comments to this blog below.