I use cookies in order to optimize my website and continually improve it. By continuing to use this site, you are agreeing to the use of cookies.
You can find an Opt-Out option and more details on the Privacy Page!

The complete CI/CD Setup (Part 4 - Postman API testing)

In this series of blog posts I’ll describe how to setup on a small environment a complete Continuous Integration and Continuous Deployment pipeline, which components are required and how to deploy them and how to make this a bit more general for additional applications.

  • In the first part I’ve made an overview and described the components that I’ll deploy.
  • In the second part we have deployed the components that are required for the pipeline.
  • In the third part we have created our first Continuous Integration and Continuous Deployment pipeline
  • In this part we will now add Postman API tests to validate after each Deployment that our application still runs as expected.

What is Postman

Postman is a tool for testing APIs and developing APIs. Most developers have used Postman to simply test APIs and to run simple HTTP Requests. But Postman can do much more, you can use Postman for example to test APIs and write tests against the response body. But you can also use Postman for continuous Monitoring of your running application to check if everything works as expected. Postman has an Enterprise Version, but most of the described things can be done also out of the box with the free version. Postman uses under the hood newman a node package that performs the requests and also executes the tests written in JavaScript.

Writing the first tests

Our application we run Continuous Integration and Continuous Deployment on via Jenkins to Kubernetes in Part 3 of the blog post series is now running and we want to check if the api runs still as expected. First we define now an environment with a variable for the url to allow for example tests against localhost and also tests for our running environment.

Create an environment with variables

Therefore we click on the Settings logo in the right box and on Add in the next Environment list dialog. Now we can define variables in our environment, in this case we define the url to our running application in Kubernetes that was deployed by Jenkins.

Create an environment with variables

After we closed the environment modal we can select now this environment from the dropdown list marked in the first picture. Now we can write our first api test and are able to use the url variable instead of writing this directly in the field:

First api call in Postman

So far so good, we write a POST call that connects to our url and sends some body inside of it, this is the typical thing we are doing most of the time. But the interresting thing is we are now able to write a test for it. Therefore we click on the Tests Tab right below the URL field and write down the following tests:

var jsonData = pm.response.json();

pm.test("Validate response content for source street", function () {
    pm.expect(jsonData.source.street).to.contain("Virchowstraße");
});
    
pm.test("Validate response content for destination street", function() {
    pm.expect(jsonData.destination.street).to.contain("Goethestraße");
});

pm.test("Validate response content for distance in Kilometer to be above 1", function() {
    pm.expect(jsonData.driving.distanceInKilometer).to.be.above(1);
});

pm.test("Validate response content for duration to be above 5 minuntes", function() {
    pm.expect(jsonData.driving.durationInMinutes).to.be.above(5);
});

pm.test("Validate response content for price to be 27 Euro", function() {
    var price = Math.trunc(jsonData.price);
    pm.expect(price).to.be.equal(27);
});

If you now Send the request again you should see the response and also an additional Tab called “Test Results”, there you can see now that the response was tested with the tests we wrote before. Now we can Save the test to a new Collection, therefore we click on Save next to the send button and create a Collection and name the test:

First api call in Postman

To have more then one test we can also write some failing test and expect the correct failure handling. Therefore we copy the existing test and change the request body to an invalid one. This allows us now to write the following test:

var jsonData = pm.response.json();

pm.test("Request is failed with status code 404", function () {
    pm.response.to.have.status(404);
});

pm.test("Validate response content to contain failure message", function () {
    pm.expect(jsonData.message).to.equals("No pricing available for the specified source and destination!");
});

When we save this now again we have two tests in the collection and can run this now automatically from inside Postman. Therefore we click on the left side on the Collections tab and on the Collection we created. Next to the name of the collection we can hover and see a play button, a new dialog opens and we can click on run. This opens a new modal where we can select our environment we created and how often the tests should be executed. After this we can click on Run, this should run the tests and we should see an overview of the test results.

Run postman collection tests

But this is not really automated, so the target is to run this Postman tests automatically after the application was deployed. Therefore we save the collection by clicking on the three dots next to the collection in the postman main screen and on Export. Now we can save the collection in format v2.1 thats also the recommended way how to save postman collections. Here you can find the tests in the git repository.

Extend the Jenkinsfile

To extend our Jenkinsfile we can now simply add a new container to our pod that uses postman/newman as image (as already said newman is the engine that executes the collections saved as json file):

containerTemplate(name: 'newman', image: 'postman/newman', ttyEnabled: true, command: 'cat')

And extend the stages with a new test stage, there we overwrite the url variable with the url from the running application:

container('newman') {
    stage("Test") {
        sh("""
            newman run \
            Demo-App-Collection.postman_collection.json \
            --env-var url=https://${url} \
            -n 2
        """)
    }
}

The resulting Jenkinsfile looks like this:

def label = "ci-cd-${UUID.randomUUID().toString()}"

podTemplate(label: label,
    serviceAccount: 'helm',
    containers: [
        containerTemplate(name: 'maven', image: 'maven:3.6-jdk-11-slim', ttyEnabled: true, command: 'cat'),
        containerTemplate(
            name: 'docker',
            image: 'docker',
            command: 'cat',
            ttyEnabled: true,
            envVars: [
                secretEnvVar(key: 'REGISTRY_USERNAME', secretName: 'registry-credentials', secretKey: 'username'),
                secretEnvVar(key: 'REGISTRY_PASSWORD', secretName: 'registry-credentials', secretKey: 'password')
            ]
        ),
        containerTemplate(name: 'helm', image: 'alpine/helm:2.11.0', command: 'cat', ttyEnabled: true),
        containerTemplate(name: 'newman', image: 'postman/newman', ttyEnabled: true, command: 'cat')
    ],
    volumes: [
        hostPathVolume(hostPath: '/var/run/docker.sock', mountPath: '/var/run/docker.sock')
    ]) {

    node(label) {
        stage('Checkout project') {
            def repo = checkout scm
            def dockerImage = "ci-cd-guide/spring-boot"
            def dockerTag = "${repo.GIT_BRANCH}-${repo.GIT_COMMIT}"
            def registryUrl = "registry.home.koudingspawn.de"
            def helmName = "spring-boot-${repo.GIT_BRANCH}"
            def url = "${helmName}.home.koudingspawn.de"
            def certVaultPath = "certificates/*.home.koudingspawn.de"
            
            container('maven') {
                stage('Build project') {
                    sh 'mvn -B clean install'
                }
            }

            container('docker') {
                stage('Build docker') {
                    sh "docker build -t ${registryUrl}/${dockerImage}:${dockerTag} ."
                    sh "docker login ${registryUrl} --username \$REGISTRY_USERNAME --password \$REGISTRY_PASSWORD"
                    sh "docker push ${registryUrl}/${dockerImage}:${dockerTag}"
                    sh "docker rmi ${registryUrl}/${dockerImage}:${dockerTag}"
                }
            }

            if ("${repo.GIT_BRANCH}" == "master") {
                container('helm') {
                    stage("Deploy") {
                        sh("""helm upgrade --install ${helmName} --namespace='${helmName}' \
                                --set ingress.enabled=true \
                                --set ingress.hosts[0]='${url}' \
                                --set ingress.annotations.\"kubernetes\\.io/ingress\\.class\"=nginx \
                                --set ingress.tls[0].hosts[0]='${url}' \
                                --set ingress.tls[0].secretName='tls-${url}' \
                                --set ingress.tls[0].vaultPath='${certVaultPath}' \
                                --set ingress.tls[0].vaultType='CERT' \
                                --set image.tag='${dockerTag}' \
                                --set image.repository='${registryUrl}/${dockerImage}' \
                                --wait \
                               ./helm""")
                    }
                }

                container('newman') {
                    stage("Test") {
                        sh("""
                            newman run \
                            Demo-App-Collection.postman_collection.json \
                            --env-var url=https://${url} \
                            -n 2
                        """)
                    }
                }
            }
        }
    }
}

And the output should look like this:

+ newman run Demo-App-Collection.postman_collection.json --env-var 'url=https://spring-boot-master.home.koudingspawn.de' -n 2
newman

Demo App Collection

Iteration 1/2

→ Calculate price for valid source and destination
  POST https://spring-boot-master.home.koudingspawn.de/api/pricing [200 OK, 587B, 798ms]
  ✓  Validate response content for source street
  ✓  Validate response content for destination street
  ✓  Validate response content for distance in Kilometer to be above 1
  ✓  Validate response content for duration to be above 5 minuntes
  ✓  Validate response content for price to be 27 Euro

→ Fail to calculate price because locations not exist
  POST https://spring-boot-master.home.koudingspawn.de/api/pricing [404 Not Found, 468B, 540ms]
  ✓  Request is failed with status code 404
  ✓  Validate response content to contain failure message

Iteration 2/2

→ Calculate price for valid source and destination
  POST https://spring-boot-master.home.koudingspawn.de/api/pricing [200 OK, 587B, 233ms]
  ✓  Validate response content for source street
  ✓  Validate response content for destination street
  ✓  Validate response content for distance in Kilometer to be above 1
  ✓  Validate response content for duration to be above 5 minuntes
  ✓  Validate response content for price to be 27 Euro

→ Fail to calculate price because locations not exist
  POST https://spring-boot-master.home.koudingspawn.de/api/pricing [404 Not Found, 468B, 513ms]
  ✓  Request is failed with status code 404
  ✓  Validate response content to contain failure message

┌─────────────────────────┬─────────────────────┬────────────────────┐
│                         │            executed │             failed │
├─────────────────────────┼─────────────────────┼────────────────────┤
│              iterations │                   2 │                  0 │
├─────────────────────────┼─────────────────────┼────────────────────┤
│                requests │                   4 │                  0 │
├─────────────────────────┼─────────────────────┼────────────────────┤
│            test-scripts │                   4 │                  0 │
├─────────────────────────┼─────────────────────┼────────────────────┤
│      prerequest-scripts │                   0 │                  0 │
├─────────────────────────┼─────────────────────┼────────────────────┤
│              assertions │                  14 │                  0 │
├─────────────────────────┴─────────────────────┴────────────────────┤
│ total run duration: 2.2s                                           │
├────────────────────────────────────────────────────────────────────┤
│ total data received: 948B (approx)                                 │
├────────────────────────────────────────────────────────────────────┤
│ average response time: 521ms [min: 233ms, max: 798ms, s.d.: 200ms] │
└────────────────────────────────────────────────────────────────────┘

So after this our application gets build, a docker image gets created on each new push and on master branch it gets also deployed and after this the postman tests are executed.

Running Jenkins pipeline with integrated postman tests

In Part 5 we will develop a concept to automatically deploy the application during Pull Requests and clean it up after the Pull Request gets closed.

Björn Wenzel

Björn Wenzel

My name is Björn Wenzel. I’m a Platform Engineer working for Schenker with interests in Kubernetes, CI/CD, Spring and NodeJS.