Skip to main content

Running An Application Locally

Every time before a change of an application is deployed to production, it is crucial to test whether everything is still working as expected. For that, we need to be able to test and/or run applications in non-productive environments, such as on a developer's machine or in a CI/CD pipeline.

Commonly, applications rely on certain assumptions regarding the runtime environment. For example, they might assume that certain services are available, or that certain environment variables are set. Unfortunately, these assumptions are often not met in non-productive environments.

In this article, we will explain different means in the context of connectivity that can help to (unit) test and/or run applications in non-productive environments.

Testing With DestinationAccessor

When using the DestinationAccessor API, applications are usually interested in retrieving destinations from the BTP Destination Service. This functionality, however, assumes that the BTP Destination Service is available (i.e. bound to the application), which is usually not the case when running locally or in a CI/CD pipeline.

Therefore, the SAP Cloud SDK offers several ways to inject destinations into the DestinationAccessor API, depending on the concrete use sceanrio. That way, applications can work even without having access to the BTP Destination Service.

Unit Test Example

In this example, we are going to demonstrate how to use the DestinationAccessor API in a unit test.

import io.vavr.control.Try;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.parallel.ResourceLock;

@BeforeEach
@AfterEach
void resetDestinationAccessor() {
DestinationAccessor.setLoader(null);
}

@Test
@ResourceLock("DestinationAccessor")
void testRemoteSystemInteraction() {
var destination = DefaultHttpDestination.builder("http://my-test-system")
.authenticationType(AuthenticationType.NO_AUTHENTICATION)
.build();
DestinationAccessor.prependDestinationLoader((name, options) -> Try.success(destination));
// optional sanity check: no matter which name we use, we always get our mocked destination
assertThat(DestinationAccessor.getDestination("any-name")).isSameAs(destination);

// TODO: perform actual test
}

The code above demonstrates how to easily inject a Destination into the DestinationAccessor API (line 14). Please note that we are using the prependDestinationLoader API to make sure our mocked destination is always returned, no matter which other DestinationLoader instances might still be registered.

Additionally, it is very important to remember that the DestinationAccessor is based on static state.
In other words: Once we are injecting a Destination (as in line 14), it will be used for all subsequent calls to the DestinationAccessor API - even when our initial test is already finished.

Therefore, it is crucial to reset the DestinationAccessor before and after each test (as in lines 3 to 7). That way, we are avoiding hard-to-debug issues that are only visible when running multiple tests in a specific order.

Furthermore, it is also recommended to use JUnit5's @ResourceLock when manipulating static state (as in this example). Using this annotation will prevent our tests from running concurrently, which could lead to unexpected results due to race conditions.

Local Run Example

In this example, we are going to demonstrate how to use the DestinationAccessor API when running an application locally. Hereby, we are making use of the EnvVarDestinationLoader, which converts an environment variable into a Destination. This class is always and automatically available, so that no code changes are required to make the following example work.

First, we need to define our destination configuration like follows:

export destinations='[{name: "my-destination", url: "https://my-remote-service.com", Authentication: "NoAuthentication" }]'
Limited Authentication Support

Destinations created that way provide support for NO_AUTHENTICATION and BASIC_AUTHENTICATION only.

In the command above, we are defining a single destination that is called my-destination and points to https://my-remote-service.com using no authentication. Please note that the destinations environment variable is a JSON array, which means that we can define multiple destinations at once.

Once that is done, we can now start our application in the same shell (i.e. where the exported environment variable is available).
Now, whenever our application uses DestinationAccessor.getDestination("my-destination"), it will receive the destination we defined in the environment variable instead of trying to retrieve it from the BTP Destination Service.

On-Premise Connectivity From Business Application Studio (BAS)

If you are developing an application in the Business Application Studio (BAS), and want to reach On-Premise systems, perform the following steps:

  1. Make sure your BAS instance is part of the same Cloud Foundry space as your Cloud Connector.
  2. Add properties WebIDEEnabled and HTML5.DynamicDestination to your On-Premise destination with the value true.
  3. In BAS, configure your destinations environment variable as follows:
[
{
"type": "HTTP",
"name": "<DESTINATION-NAME>",
"url": "http://<DESTINATION-NAME>.dest",
"ProxyHost": "localhost",
"ProxyPort": 8887
}
]

Please refer to the BAS connectivity guide created for the SAP Cloud SDK for JavaScript to get more information and a detailed description of the technical background. The information there also applies to the SAP Cloud SDK for Java.

On-Premise Connectivity From Local IDE

The SAP BTP connectivity service builds the connection between SAP BTP and the On-premise network. That is why it has strong built-in restrictions to allow it only to be called from within the SAP Business Technology Platform. If you call the connectivity service from your local machine, you will encounter a connection timeout. We'll therefore apply port forwarding via SSH to simulate that your localhost plays the cloud app.

  • Deploy your app to the SAP BTP once.
  • Enable SSH access to your app container with the cf CLI:
cf enable-ssh app-name
cf restart app-name
  • Inspect the value of the entry connectivity of your VCAP_SERVICES and take note of the values of the fields

    • credentials.onpremise_proxy_port (we will refer to the value as proxy-port hereafter)
    • credentials.onpremise_proxy_host (we will refer to the value as proxy-host hereafter)
  • Create an SSH session to your app container with the following command and let the session opened:

cf ssh app-name -L proxy-port:proxy-host:proxy-port
  • Copy over the VCAP_SERVICES of your actually deployed app to your local machine so that the app can access it.
    • Tip Many IDEs provide support for using the content of a file (e.g. default-env.json) as environment variables when starting your application. This way, you can avoid the hustle of creating the environment variables yourself. Please refer to the documentation of your IDE to learn more about this feature.
  • Replace the value of the field VCAP_SERVICES.connectivity.credentials.onpremise_proxy_host with localhost.
    • That way, the SAP Cloud SDK will use the SSH tunnel instead of the real connectivity service.