Menu

How to automate API testing with Azure Devops and Postman?

What is Postman?

Postman is an API platform for building and using APIs. Postman simplifies each step of the API lifecycle and streamlines collaboration so you can create better APIs—faster. Source: Postman API Platform

I have been using Postman for a long time to test API requests from my local machine. I started to investigate during one project what kind of automated integration test possibilities could Postman offer. Actually it offered very nice features what I haven't used before. Best finding was possibility to integrate everything very easily to Azure DevOps pipelines.

Useful articles

How to configure Postman API requests?

Prerequisites

Presented test requests in this blog post are written against this kind of Product API. This Minimal API provides REST endpoints to interactive with products.

  public static class EndpointRouteBuilderExtensions
    {
        public static void AddProductApiEndpoints(this IEndpointRouteBuilder endpoints)
        {
            endpoints.MapPost("/products", async ([FromBody] Product product, [FromServices] ProductRepository productRepository) =>
            {
                await productRepository.SaveAsync(product);
                return Results.Created($"/products/{product.Id}", product);
            })
            .RequireAuthorization("ProductsPolicy");

            endpoints.MapGet("/products", async ([FromServices] ProductRepository productRepository) =>
            {
                var products = await productRepository.ListAsync();
                return products?.Count > 0 ? Results.Ok(products) : Results.NoContent();
            })
            .RequireAuthorization("ProductsPolicy");

            endpoints.MapPut("/products/{productId}", async ([FromUri] int productId, [FromBody] Product product, [FromServices] ProductRepository productRepository) =>
            {
                await productRepository.UpdateAsync(productId, product);
                return Results.Ok();
            })
            .RequireAuthorization("ProductsPolicy");

            endpoints.MapGet("/products/{productId}", async ([FromUri] int productId, [FromServices] ProductRepository productRepository) =>
            {
                var product = await productRepository.GetAsync(productId);
                return product == null ? Results.NoContent() : Results.Ok(product);
            })
            .RequireAuthorization("ProductsPolicy");

            endpoints.MapDelete("/products", async ([FromServices] ProductRepository productRepository) =>
            {
                await productRepository.DeleteAllAsync();
            })
            .RequireAuthorization("ProductsPolicy");
        }
    }

Let's get started. Open Postman client:)

1. Set Postman environment variables

These environment variables contains all required information to fetch bearer token from the OIDC token endpoint. In this sample I will use Duende demo server as a OIDC provider.

undefined

Get more information about variable scopes from here.

2. Create a collection for your requests

Separate collection makes easier to maintain and configure requests because collection level enables to use shared pre-request scripts which are executed before every request inside the collection.

undefined

3. Shared pre-request scripts in collection level

You can configure pre-request scripts when you have selected the collection.

undefined

Because API requires authentication/authorization we need to get bearer token before reaching the API. The purpose of this pre-request script is to fetch bearer token from OIDC token endpoint. All configuration values like ClientId and Secret are fetched from the environment variables.

var Header = require('postman-collection').Header;

pm.sendRequest({
    url: pm.environment.get("TokenEndpointUrl"),
    method: 'POST',
    header: {
        'Content-Type': 'application/x-www-form-urlencoded'
    },
    body: {
        mode: 'urlencoded',
        urlencoded: [{
                key: "grant_type",
                value: 'client_credentials',
                enabled: true
            },
            {
                key: "client_id",
                value: pm.environment.get("ClientId"),
                enabled: true
            },
            {
                key: "client_secret",
                value: pm.environment.get("ClientSecret"),
                enabled: true
            },
            {
                key: "scope",
                value: pm.environment.get("ClientScopes"),
                enabled: true
            }
        ]
    }
}, function(err, res) {
    pm.request.headers.append(Header.create('Bearer ' + res.json().access_token, 'Authorization'));
});

Now bearer token is fetched every time before actual request is executed. You could also store fetched bearer token to the variable to minimize token fetches.

4. Test requests

Let's start to write actual request tests. Each request has Tests tab where Tests can be written.

undefined

Example test plan

The following tests are created to test product creation, update, delete and get operations. Next I'll present sample CURL command and actual Postman test written with Javascript.

Add new product

curl --location --request POST '/products' \
--header 'Content-Type: application/json' \
--data-raw '{
  "name":"Product 1",
  "price": 0.2
}'

This test verifies that product is successfully created. Status code must be 201 (Created) and header must contain path to product 1.

pm.test("Status code is 201", function () {
    pm.response.to.have.status(201);
});

pm.test("Location header is /products/1", function () {
    pm.expect(pm.response.headers.get("location")).to.eql("/products/1")
});

List all products

curl --location --request GET '/products'

This test verifies that newly created product exists. Status code must be 200 (OK) and length of the product arrays must be 1. Test also verifies that existing product is Product 1.

pm.test("Status code is 200", () => {
    pm.response.to.have.status(200);
});

pm.test("One product found", () => {
    const responseJson = pm.response.json();
    pm.expect(responseJson.length).to.eql(1);
});

pm.test("Right product found", () => {
    const responseJson = pm.response.json();
    pm.expect(responseJson[0].name).to.eql("Product 1");
    pm.expect(responseJson[0].price).to.eql(0.2);
});

Update product

curl --location --request PUT '/products/1' \
--header 'Content-Type: application/json' \
--data-raw '{
  "id": 1,
  "name": "Product 1 (fixed name)",
  "price": 0.3
}'

This test verifies that product is successfully updated. Status code must be 200 (OK).

pm.test("Status code is 200", function () {
    pm.response.to.have.status(200);
});

Get Product

curl --location --request GET '/products/1'

This test verifies that updated Product 1 exists. Status code must be 200 (OK).

pm.test("Status code is 200", function () {
    pm.response.to.have.status(200);
});

pm.test("Product 1 found", () => {
    const responseJson = pm.response.json();
    pm.expect(responseJson.id).to.eql(1)
    pm.expect(responseJson.name).to.eql("Product 1 (fixed name)");
    pm.expect(responseJson.price).to.eql(0.3);
});

Delete all products

curl --location --request DELETE '/products'

This test deletes all products. Status code must be 200 (OK).

pm.test("Status code is 200", function () {
    pm.response.to.have.status(200);
});

Verify that products are deleted

curl --location --request GET '/products' 

This test verifies that all products are deleted. Status code must be 204 (No Content).

pm.test("Status code is 204", function () {
    pm.response.to.have.status(204);
});

Configure tests to be executed in Azure DevOps

1. Export test requests and environment variables

Postman provides Export functionality which creates a JSON file from your collection requests and environment variables.

2. Remove Client Secret from environment JSON configuration

Current setup has sensitive information (ClientId and ClientSecret) stored in environment variables so these must be removed from Environment.json file before commiting it ex. to source control.

3. Create YAML template

Client Id and Client Secret are fetched from the Pipeline variables where those are configured to be kept as a secret value.

undefined

Pipeline variable values are then passed to the integration tests pipeline template.

- template: pipelines/integration-tests-pipeline.yml
  parameters:
    clientId: $(ApiClientId)
    clientSecret: $(ApiClientSecret)

Integration tests pipeline template fetches integration tests artifact and installs CLI tool called "Newman" which is used to execute Postman tests in Azure DevOps.

parameters:
    clientId: string
    clientSecret: string

stages:
- stage: IntegrationTests
  displayName: Integration Tests
  jobs:

  - job: IntegrationTests
    displayName: Execute integration tests
    steps:
    
    - task: DownloadBuildArtifacts@0
      displayName: 'Download integration tests artifact'
      inputs:
        buildType: 'current'
        downloadType: 'single'
        artifactName: 'tests'
        downloadPath: '$(System.ArtifactsDirectory)'  

    - task: Npm@1
      displayName: Install newman CLI
      inputs:
        command: custom
        verbose: false
        customCommand: install -g newman

    - task: CmdLine@2
      displayName: Execute integration tests (API Client 1)
      inputs:
        script: newman run $(System.ArtifactsDirectory)/tests/IntegrationTests.json -e $(System.ArtifactsDirectory)/tests/Environment.json --env-var ClientId=${{parameters.clientId}} --env-var ClientSecret=${{parameters.clientSecret}}

Newman has --env-var parameter which enables to pass sensitive configuration values from ex. Azure DevOps pipeline variables. 

Summary

Postman provides an easy and powerful way to create automated integration tests which can be executed ex. in Azure DevOps. I was really nice that you could write Postman test scripts with javascript. Test object model is easy to learn and there are lot of good documentation available. At least for a simple integration cases this is a very good tool. Sharing test scripts between tests is difficult and one reason why you should consider other frameworks in very complex test cases.

Comments