Contact Us 1-800-596-4880

Writing Integration Tests

Flex Gateway Policy Development Kit (PDK) provides an automated testing framework to detect integration regressions in your custom policy.

The integration testing framework provided by PDK only supports Flex Gateway running in Local Mode in a Docker container. If you plan to deploy or policy to Flex Gateway running in Connected Mode or to a different platform, you should still first test your policy in PDK by using Flex Gateway running in Local Mode in a Docker container.

You can run Flex Gateway in Local Mode on Docker in Windows only to develop and test your policies. However, Flex Gateway does not support Windows in production environments.

To test your policy, complete the following tasks:

Before You Begin

Ensure that you have:

Integration Tests Directory Structure

By default, the <root-directory>/tests directory contains the following files:

tests
├── common.rs
├── requests.rs
├── common
│   └── logging.yaml
│   └── registration.yaml
└── requests
    └── hello
        └── api.yaml

The /tests directory contains the integration tests. By default, the folder contains:

  • requests.rs file: An individual test module and executable.

  • commons.rs file: A test module that contains functionalities that must be included in every integration test module.

Each test module has a configuration directory. By default, /test includes the following folders:

  • /requests

  • /common

Every module contains test functions annotated with the #[pdk_test] attribute. Each test function also has a configuration subdirectory. For example, the requests::hello test function has the corresponding requests/hello directory containing all the hello test configuration files.

Register a Flex Gateway Instance in Local Mode

To begin using the make test command to debug your custom policy, register a Flex Gateway in Local Mode.

For the make test command to work, a Flex Gateway registration.yaml file must exist in the <root-directory>/tests/common directory. Create this file by running the registration command in the directory, or move a registration.yaml file of a previously registered Flex Gateway to the directory.

To register Flex Gateway, see:

Run only the registration command. Do not run the Docker start command because the make test command completes this step.

Create a different registration file on each device testing the policy.

The registration file is ignored for storage in remote repositories and is listed in the .gitignore file located at the root of the project directory.

Configure Your Custom Policy

Each test function requires an api.yaml file to configure the custom policy for that request. PDK provides the request/hello/api.yaml file as an example. You can copy and paste the example file into the resource folders of your test functions.

To configure the custom policy:

  1. Print the policy ID by running the show-policy-ref-name:

    make show-policy-ref-name
  2. Copy the result.

  3. Override {{project-name}} in api.yaml with your policy ID, for example:

        policies:
        - policyRef:
            name: <custom-policy-id>
  4. Configure the policy’s configuration parameters.

    To find your configuration parameters, see the gcl.yaml file.

  5. Copy and past the api.yaml into the resource folders of your other test functions.

Create a Test Function

Add a new test module by placing a new test function in an existing test module, for example /tests/requests.rs.

PDK test functions are async functions decorated with the #[pdk_test] attribute:

use pdk_test::pdk_test;

#[pdk_test]
async fn say_hello() {

}
Because the body is empty, this test function never fails. Run make test to ensure everything is working fine.

Run an Integration Test

To run the integrations tests modules in the /tests folder, execute the make test command:

make test

Run this test as you add additional features to your integration test to ensure proper configuration.

The make test command compiles the policy before running the integration tests.

Configure Services for Integration Testing

PDK integration tests contain a composite of services running inside a Docker network. You can register service instances in a TestComposite object. Every service has a configuration, hostname, and instance.

The pdk-test crate contains all the functionality for writing integration tests for custom policies in PDK and is included in the [dev-dependencies] section of the Cargo.toml file.

Supported Services

PDK’s test library supports the following services:

Service Description

Flex

Flex Gateway service instance.

httpmock

Service binding to the Rust’s httpmock server.

The supported services cover the majority of use cases for integration testing. It is not possible to define different types of service.

httpmock Service

httpmock is a mock server that enables you to write request mocks in Rust. Configure an httpmock service by using an HttpMockConfig value. It is a best practice to return a Result from the test function. The anyhow Rust crate enables you to return different default error types.

To configure an httpmock service, see the following code example:

use pdk_test::{pdk_test, TestComposite};
use pdk_test::services::httpmock::HttpMockConfig;

#[pdk_test]
async fn say_hello() -> anyhow::Result<()> {

    // Configure HttpMock service
    let backend_config = HttpMockConfig::builder()
        .hostname("backend")
        .port(80) // Port where the service will accept requests
        .build();

    // Register HTTPBin service and start the docker network
    let composite = TestComposite::builder()
        .with_service(backend_config)
        .build()
        .await?;

    Ok(())
}
If the Docker engine is properly configured, this test passes. Run make test to ensure proper configuration.

Get a Service’s Handle

Every configured service instance in the composite has a handle for custom interaction. The handle returns the sockets where the service endpoints are available.

To find a service’s handle, see the following code example:

use pdk_test::{pdk_test, TestComposite};
use pdk_test::services::httpmock::{HttpMock, HttpMockConfig};

#[pdk_test]
async fn say_hello() -> anyhow::Result<()> {

    // Configure HTTPMock service
    let backend_config = HttpMockConfig::builder()
        .hostname("backend")
        .build();

    // Register HTTPMock service and start the docker network
    let composite = TestComposite::builder()
        .with_service(backend_config)
        .port(80) // Port where the service accepts requests
        .build()
        .await?;

    // Get the httpmock handle
    let httpmock: HttpMock = composite.service()?;

    Ok(())
}
If the httpmock instance is properly configured, this test passes. Run make test to ensure proper configuration.

HTTP Endpoint Requests

To mock endpoints, use the httpmock When/Then API.

After mocking the endpoint, use an HTTP client to make endpoint requests.

The reqwest crate offers the most comprehensive and flexible Rust HTTP client.

To mock endpoints and make requests, see the following code example:

use pdk_test::{pdk_test, TestComposite};
use pdk_test::services::httpmock::{HttpMock, HttpMockConfig};

#[pdk_test]
async fn say_hello() -> anyhow::Result<()> {

    // Configure HttpMock service
    let backend_config = HttpMockConfig::builder()
        .hostname("backend")
        .port(80) // Port where the service will accept requests
        .build();

    // Register HTTPBin service and start the docker network
    let composite = TestComposite::builder()
        .with_service(backend_config)
        .build()
        .await?;

    // Get the httpmock handle
    let httpmock: HttpMock = composite.service()?;

    // Connect the mock server
    let mock_server = httpmock::MockServer::connect_async(httpmock.socket()).await;

    // Configure the endpoint mocks
    mock_server.mock_async(|when, then| {
        when.path_contains("/hello");
        then.status(202).body("World!");
    }).await;

    let base_url = mock_server.base_url();

    // Hit the endpoint
    let response = reqwest::get(format!("{base_url}/hello")).await?;

    // Assert on response
    assert_eq!(response.status(), 202);
    assert_eq!(response.text().await?, "World!");

    Ok(())
}
Run make test to ensure proper configuration.

Configure a Flex Service Instance

Configure a Flex service by registering a FlexConfig in TestComposite.

Flex services have the following configurable properties:

Property Content

version

Flex Gateway version to test.

hostname

Hostname of the Flex service. By default, "local-flex".

config_mounts

A map of local directories where the Flex service finds configuration files. POLICY_DIR and COMMON_CONFIG_DIR constants are defined in the common.rs module when the project is created.
Add the directory with the specific configuration for the actual test function, for example, the hello test uses SAY_HELLO_CONFIG_DIR.

ports

A List of ports where the Flex service is listening for requests. It must coincide with the ports configured in the spec.address property in the api.yaml configuration file.

To configure a Flex service instance, see the following code example:

mod common;
use common::*;

use pdk_test::{pdk_test, TestComposite};
use pdk_test::port::Port;
use pdk_test::services::flex::FlexConfig;

// Directory with the configurations for the `say_hello` test.
const SAY_HELLO_CONFIG_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/requests/say_hello");

// Port where Flex is waiting for requests
const FLEX_PORT: Port = 8081;

#[pdk_test]
async fn say_hello() -> anyhow::Result<()> {

    // Configure a Flex service
    let flex_config = FlexConfig::builder()
        .version("1.6.1")
        .hostname("local-flex")
        .config_mounts([
            (POLICY_DIR, "policy"),
            (COMMON_CONFIG_DIR, "common"),
            (SAY_HELLO_CONFIG_DIR, "say_hello"),
        ])
        .ports([FLEX_PORT])
        .build();

    let composite = TestComposite::builder()
        .with_service(flex_config)
        .build()
        .await?;
}
Run make test to ensure proper configuration.

Flex Service Endpoint Requests

Flex service exposes endpoints with external base URLs dependent on the port. To access those URLs, the Flex service instance handle provides the external_url() method indexed by port, for example:

use pdk_test::{pdk_test, TestComposite, port::Port};
use pdk_test::services::flex::Flex;

const FLEX_PORT: Port = 8081;

async fn build_composite() -> anyhow::Result<TestComposite> {
    todo!("Configuration stuff comes here")
}

#[pdk_test]
async fn say_hello() -> anyhow::Result<()> {
    // Invoke a helper composite builder function for simplicity
    let composite = build_composite().await?;

    // Get the Flex service handle
    let flex: Flex = composite.service()?;

    // Get the URL for our configured port
    let flex_url = flex.external_url(FLEX_PORT).unwrap();

    let response = reqwest::get(format!("{flex_url}/hello")).await?;

    // Make assertions

    Ok(())
}

Combine Multiple Services

Usually a PDK integration test includes a Flex service and at least one backend service.

The following code is the default test generated when the custom policy is created:

mod common;

use httpmock::MockServer;
use pdk_test::{pdk_test, TestComposite};
use pdk_test::port::Port;
use pdk_test::services::flex::{FlexConfig, Flex};
use pdk_test::services::httpmock::{HttpMockConfig, HttpMock};

use common::*;

// Directory with the configurations for the `hello` test.
const HELLO_CONFIG_DIR: &str =  concat!(env!("CARGO_MANIFEST_DIR"), "/tests/requests/hello");

// Flex port for the internal test network
const FLEX_PORT: Port = 8081;

// This integration test shows how to build a test to compose a local-flex instance
// with a MockServer backend
#[pdk_test]
async fn hello() -> anyhow::Result<()> {

    // Configure a Flex service
    let flex_config = FlexConfig::builder()
        .version("1.6.1")
        .hostname("local-flex")
        .ports([FLEX_PORT])
        .config_mounts([
            (POLICY_DIR, "policy"),
            (COMMON_CONFIG_DIR, "common"),
            (HELLO_CONFIG_DIR, "hello"),
        ])
        .build();

    // Configure an HttpMock service
    let httpmock_config = HttpMockConfig::builder()
        .port(80)
        .version("latest")
        .hostname("backend")
        .build();

    // Compose the services
    let composite = TestComposite::builder()
        .with_service(flex_config)
        .with_service(httpmock_config)
        .build()
        .await?;

    // Get a handle to the Flex service
    let flex: Flex = composite.service()?;

    // Get an external URL to point the Flex service
    let flex_url = flex.external_url(FLEX_PORT).unwrap();

    // Get a handle to the HttpMock service
    let httpmock: HttpMock = composite.service()?;

    // Create a MockServer
    let mock_server = MockServer::connect_async(httpmock.socket()).await;

    // Mock a /hello request
    mock_server.mock_async(|when, then| {
        when.path_contains("/hello");
        then.status(202).body("World!");
    }).await;

    // Perform an actual request
    let response = reqwest::get(format!("{flex_url}/hello")).await?;

    // Assert on the response
    assert_eq!(response.status(), 202);

    Ok(())
}

Review Service Logs

After an integration test fails, you can view the service logs to find errors. To improve the debugging experience, there is a dedicated service log in each test folder at <root-directory>/target/pdk-test/<module-name>/<test>/<service>.log.

For the example, the say_hello test has the following service log:

<root-directory>
├── Cargo.lock
├── Cargo.toml
├── playground
├── src
├── tests
└── target
    └── pdk-test
        └── requests
            └── say_hello
                └── <service>.log