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

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, API configurations, and policy configurations that must be included in every integration test module.

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 the configuration of the API and policies for that request.

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 commons.rs with your policy ID, for example:

    pub const POLICY_NAME: &str = "<custom-policy-id>";
  4. Configure the policy’s configuration parameters.

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

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 an instance of the FlexConfig struct in TestComposite. To complete the FlexConfig instance, you must also define a ApiConfig and PolicyConfig instance.

The FlexConfig struct has the following 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. The POLICY_DIR and COMMON_CONFIG_DIR constants are defined in the common.rs module by default. If your test requires further configuration, add a directory with the specific configuration for the test functions.

with_api

The ApiConfiguration of the API deployed to the Flex service instance. Call multiple times to configure multiple APIs.

ports

List of ports where the Flex service listens for requests. If any of your configuration directories included in config_mount directories expose an API, the port must match the ports configured in the spec.address property.

The ApiConfig has the following properties:

Property Content

name

Name of the API.

upstream

The reference to the upstream service.

path

Path to which the upstream requests are sent.

port

Ports where the API listens for requests.

policies

A List of PolicyConfig instances applied to the API.

The PolicyConfig has the following properties:

Property Content

name

Policy to apply.

configuration

Parameters for the policy.

The following code example defines a complete Flex service:

mod common;
use common::*;

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

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

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

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

    // Configure a policy for the API
    let policy_config = PolicyConfig::builder()
        .name(POLICY_NAME)
        .configuration(serde_json::json!({"source": "http://backend/blocked", "frequency": 60}))
        .build();

    // Configure the API to deploy to the Flex
    let api_config = ApiConfig::builder()
        .name("ingress-http")
        .upstream(&httpmock_config)
        .path("/anything/echo/")
        .port(FLEX_PORT)
        .policies([policy_config])
        .build();

    // Configure the Flex service
    let flex_config = FlexConfig::builder()
        .version("1.6.1")
        .hostname("local-flex")
        .config_mounts([
            (POLICY_DIR, "policy"),
            (COMMON_CONFIG_DIR, "common")
        ])
        .with_api(api_config)
        .build();

    let composite = TestComposite::builder()
        .with_service(flex_config)
        .with_service(httpmock_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 an HttpMock service
    let httpmock_config = HttpMockConfig::builder()
        .port(80)
        .version("latest")
        .hostname("backend")
        .build();

    // Configure a policy for the API
    let policy_config = PolicyConfig::builder()
        .name(POLICY_NAME)
        .configuration(serde_json::json!({"source": "http://backend/blocked", "frequency": 60}))
        .build();

    // Configure the API to deploy to the Flex
    let api_config = ApiConfig::builder()
        .name("ingress-http")
        .upstream(&httpmock_config)
        .path("/anything/echo/")
        .port(FLEX_PORT)
        .policies([policy_config])
        .build();

    // Configure the Flex service
    let flex_config = FlexConfig::builder()
        .version("1.6.1")
        .hostname("local-flex")
        .config_mounts([
            (POLICY_DIR, "policy"),
            (COMMON_CONFIG_DIR, "common")
        ])
        .with_api(api_config)
        .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