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" }]'
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:
- Make sure your BAS instance is part of the same Cloud Foundry space as your Cloud Connector.
- Add properties
WebIDEEnabled
andHTML5.DynamicDestination
to your On-Premise destination with the valuetrue
. - 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 yourVCAP_SERVICES
and take note of the values of the fieldscredentials.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.
- Tip Many IDEs provide support for using the content of a file (e.g.
- Replace the value of the field
VCAP_SERVICES.connectivity.credentials.onpremise_proxy_host
withlocalhost
.- That way, the SAP Cloud SDK will use the SSH tunnel instead of the real connectivity service.