Karate Series | Session 7 | Karate Reports

What we explore in this series

Karate Basics
GET API
POST API
PUT API
DELETE API

Karate Config
Karate Reports

Karate has a built-in native HTML reporting feature, which should ideally serve all your needs. Still, you can enable cucumber reporting, which would also generate reports in the form of HTML.

If you wanted to enable the cucumber reporting feature, the following dependency to be added to the test scope. We had already discussed this in brief in the first session of this series.

<dependency>
    <groupId>net.masterthought</groupId>
    <artifactId>cucumber-reporting</artifactId>
    <version>5.3.0</version>
    <scope>test</scope>
</dependency>

We can easily integrate karate reporting with the CI tools as most of such tools would be able to process the output of the parallel runner and determine the status of the build as well as generate reports.

The native reporting feature does not only create an HTML file but also creates feature-wise output files in the form of XML and JSON. These output files will be consumed by third-party plugins like cucumber and generate catching HTML reports of their own.

There also exists another maven reporting plugin that is compatible with Karate JSON called Cluecumber.

Karate Reports

Once you run the TestRunner class (we’ve already gone through TestRunner in the first session of this series), all the feature files will get executed, and an HTML file called karate-summary.html will be generated in the following path.

<project-root-directory>/target/surefire-reports

Apart from this summary file, feature-wise JSON and XML files will also be generated in the same directory. The directory will also contain another HTML file called timeline.html which shows an overall picture of the parallel tests done and says how much time was taken for each test.

Here’s the karate-summary.html


This is what we get if we click on one of the features given in the above picture. All the steps for each scenario belonging to that feature will be listed and the time taken for each step will also be given.


Here’s the timeline.html

Cucumber Reports

If the cucumber reporting feature is enabled and the TestRunner gets run, it will create a file named overview-features.html in the following path. The path is customizable, and we can set this output directory location in the Configuration class of the Cucumber plugin.

<project-root-directory>/src/test/java/reports/cucumber-html-reports

Cucumber will read the JSON files generated by the Karate and generate a report of its own in this directory. So the code for generating cucumber reports has been written in the TestRunner class and the class would look like this.

import com.intuit.karate.KarateOptions;
import com.intuit.karate.Results;
import com.intuit.karate.Runner;
import com.intuit.karate.junit4.Karate;
import net.masterthought.cucumber.Configuration;
import net.masterthought.cucumber.ReportBuilder;
import org.apache.commons.io.FileUtils;
import org.junit.BeforeClass;
import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;

import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;


@RunWith(Karate.class)
@KarateOptions(features = "classpath:features", tags = "~@ignore")
public class TestRunner {

    @BeforeClass
    public static void init() {
        System.setProperty("karate.env", "local");
    }

    @Test
    public void testParallel() {
        Results results = Runner.path("classpath:features").tags("~@ignore").parallel(4);
        generateReport(results.getReportDir());
    }

    public static void generateReport(String outputPath) {
        Collection<File> jsonFiles = FileUtils.listFiles(new File(outputPath), new String[]{"json"}, true);
        List<String> jsonPaths = new ArrayList<>(jsonFiles.size());
        jsonFiles.forEach(file -> jsonPaths.add(file.getAbsolutePath()));
        Configuration config = new Configuration(new File("./src/test/java/reports"), "karate-api-automation");
        new ReportBuilder(jsonPaths, config).generateReports();
    }
}

This is how cucumber report looks like


If we click on one of the features, like Karate reports, each individual step can be seen.


If one of the tests gets failed, it will report like this

Finally, this is the project structure

So, here the Karate series has come to an end. We will catch up with another one soon.

Karate Series | Session 6 | Karate Config

What we explore in this series

Karate Basics
GET API
POST API
PUT API
DELETE API

Karate Config
Karate Reports

Karate config is nothing but a javascript function that returns a JSON object. The config file should be named karate-config.js and placed under classpath (src/test/java). At the time of startup, Karate will search for the config file in the classpath and read the configurations automatically.

The config file plays a crucial role in the application. It can be used to store global variables, set URLs, headers, authentication details, etc. It can also be used to define environments and the values of the variables can be dynamically changed based on the running environment.

The karate-config.js will be re-processed for every scenario. We can change this behavior by calling karate.callSingle() function.

A sample file is given below.

function fn() {
    var config = {
        author: 'Deepesh Darshan',
        baseURL: 'https://reqres.in/api'
    };

    var env = karate.env;
    karate.log('Given Env: ', env);
    if (env == 'dev') {
        config.baseURL = 'https://reqres.in/dev/api'
    } else if (env == 'prod') {
        config.baseURL = 'https://reqres.in/prod/api'
    } else if (env == 'local') {
        config.baseURL = 'https://reqres.in/api'
    }

    karate.configure('connectTimeout', 5000);
    karate.configure('readTimeout', 5000);
    return config;
}

Here, the baseURL is set as https://reqres.in/api. We can dynamically change the value of this variable based on the current running environment. We can set configuation settings like connectTimeout and readTimeout in the config file. We can also configure proxy settings, headers, cookies, etc in the config file.

The environment can be defined either in the TestRunner.java or by the maven command line, and it will be taken on runtime by the Karate.

// Java configuration
System.setProperty("karate.env", "local");

//mvn command line
mvn test -DargLine="-Dkarate.env=prod"

Now let’s go through different scenarios.

Scenario 1

Accessing config file value

  #Accessing config file value
  Scenario: Config Author Demo
    Given print author

We’ve already defined a global variable namely author and set its value ‘Deepesh Darshan‘ in karate-config.js. Karate will automatically read the config file and read the variable. So in the feature file, we can directly access the variable.

See the output of the above scenario:

17:16:43.553 [pool-1-thread-1] INFO com.intuit.karate - found scenario at line: 8 - ^Config Author Demo$
17:16:43.702 [ForkJoinPool-1-worker-1] INFO com.intuit.karate - Given Env:   
17:16:43.755 [ForkJoinPool-1-worker-1] DEBUG com.jayway.jsonpath.internal.path.CompiledPath - Evaluating path: $
17:16:43.775 [ForkJoinPool-1-worker-1] INFO com.intuit.karate - [print] Deepesh Darshan

Scenario 2

Accessing config file and calling GET API

Feature: Config Demo  

  Background:
    * url baseURL
    * header Accept = 'application/json'

  #Get request with config file value
  Scenario: Config Get Demo 1
    Given path '/users?page=2'
    When method GET
    Then status 200
    And print response

The baseURL, declared in the background, will get its value from the config file (it will take the default value, as the environment is not yet set), and using this we can run the scenario. Please note, in the given scenario, we never defined or declared any URLs, but still, it will call the GET API. The output of the scenario is given below:

17:20:57.082 [ForkJoinPool-1-worker-1] DEBUG com.jayway.jsonpath.internal.path.CompiledPath - Evaluating path: $
17:20:57.083 [ForkJoinPool-1-worker-1] INFO com.intuit.karate - [print] {
  "per_page": 6,
  "total": 12,
  "data": [
    {
      "color": "#98B2D1",
      "year": 2000,
      "name": "cerulean",
      "id": 1,
      "pantone_value": "15-4020"
    },
    {
      "color": "#C74375",
      "year": 2001,
      "name": "fuchsia rose",
      "id": 2,
      "pantone_value": "17-2031"
    },
    {
      "color": "#BF1932",
      "year": 2002,
      "name": "true red",
      "id": 3,
      "pantone_value": "19-1664"
    },
    {
      "color": "#7BC4C4",
      "year": 2003,
      "name": "aqua sky",
      "id": 4,
      "pantone_value": "14-4811"
    },
    {
      "color": "#E2583E",
      "year": 2004,
      "name": "tigerlily",
      "id": 5,
      "pantone_value": "17-1456"
    },
    {
      "color": "#53B0AE",
      "year": 2005,
      "name": "blue turquoise",
      "id": 6,
      "pantone_value": "15-5217"
    }
  ],
  "page": 1,
  "total_pages": 2,
  "support": {
    "text": "To keep ReqRes free, contributions towards server costs are appreciated!",
    "url": "https://reqres.in/#support-heading"
  }
}

Scenario 3

Calling a java method

In Karate, we can directly call java functions also. Here in this scenario, we have created a simple java class namely BdayWishHelper.java, and written a public method namely wish inside it. The method will accept a parameter of type string and return some string value.

package com.example.karate.helper;

public class BdayWishHelper {

    public String wish(final String name) {
        return "Happy Birthday " + name;
    }

}

We will call this method from the scenario and print the response.

  Scenario: Calling Java Method
    * def wishHelper =
    """
    function() {
      var Helper = Java.type('com.example.karate.helper.BdayWishHelper');
      var helperInstance = new Helper();
      return helperInstance.wish('Sourav');
    }
    """
    * def wish = call wishHelper
    * print wish

And, here’s the response:

18:05:38.136 [pool-1-thread-1] INFO com.intuit.karate - found scenario at line: 18 - ^Calling Java Method$
18:05:38.263 [ForkJoinPool-1-worker-1] INFO com.intuit.karate - Given Env:   
18:05:38.318 [ForkJoinPool-1-worker-1] DEBUG com.jayway.jsonpath.internal.path.CompiledPath - Evaluating path: $
18:05:38.356 [ForkJoinPool-1-worker-1] INFO com.intuit.karate - [print] Happy Birthday Sourav

Scenario 4

Calling a Springboot endpoint

There’s nothing special in it as calling a REST endpoint has already been covered in one of the previous posts. The only change here is, we’ve created our own endpoint using Springboot this time and called it from Karate. So let’s see the GET endpoint.

KarateController.java

package com.example.karate.controller;

import com.example.karate.service.KarateService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api")
public class KarateController {

    private KarateService service;

    @Autowired
    private KarateController(KarateService service) {
        this.service = service;
    }

    @RequestMapping("/hello/{name}")
    public String hello(@PathVariable String name) {
        return service.hello(name);
    }
}

KarateService.java

package com.example.karate.service;

public interface KarateService {
    public String hello(final String name);
}

KarateServiceImpl.java

package com.example.karate.service;

import org.springframework.stereotype.Service;

@Service("karateService")
public class KarateServiceImpl implements KarateService {

    @Override
    public String hello(final String name) {
        return "Hello "+name;
    }
}

So, this endpoint does nothing but accepts an input parameter of type string and returns some string value. This is how we call this endpoint from Karate.

    Scenario: Springboot
      Given url 'http://localhost:8050/api/hello/Sachin'
      When method GET
      Then status 200
      And print response

And, here’s the response:

18:20:44.569 [ForkJoinPool-1-worker-1] DEBUG org.apache.http.impl.conn.PoolingHttpClientConnectionManager - Connection released: [id: 0][route: {}->http://localhost:8050][total available: 1; route allocated: 1 of 5; total allocated: 1 of 10]
18:20:44.569 [ForkJoinPool-1-worker-1] DEBUG com.intuit.karate - response time in milliseconds: 118.72
1 < 200
1 < Connection: keep-alive
1 < Content-Length: 12
1 < Content-Type: application/json
1 < Date: Tue, 15 Nov 2022 12:50:44 GMT
1 < Keep-Alive: timeout=60
Hello Sachin

Here’s the full feature file.

Feature: Config Demo

  Background:
    * url baseURL
    * header Accept = 'application/json'

  #Accessing config file value
  Scenario: Config Author Demo
    Given print author

  #Get request with config file value
  Scenario: Config Get Demo 1
    Given path '/users?page=2'
    When method GET
    Then status 200
    And print response

  Scenario: Calling Java Method
    * def wishHelper =
    """
    function() {
      var Helper = Java.type('com.example.karate.helper.BdayWishHelper');
      var helperInstance = new Helper();
      return helperInstance.wish('Sourav');
    }
    """
    * def wish = call wishHelper
    * print wish

    Scenario: Springboot
      Given url 'http://localhost:8050/api/hello/Sachin'
      When method GET
      Then status 200
      And print response

In this session, we’ve seen how to write config and make use of it in Karate. That’s all for now. Thank you!

Karate Series | Session 5 | DELETE API

What we explore in this series

Karate Basics
GET API
POST API
PUT API
DELETE API
Karate Config
Karate Reports

As the name says, DELETE API is used to delete a resource on the server. Some conversations are roaming around like whether the DELETE request can be considered as idempotent. According to RFC-2616, since GET, HEAD, PUT and DELETE does not have any side effects, these can be considered as idempotent.

So, here we are going to see the scenario which sends requests to https://reqres.in/api/users API.

Given the feature file for DELETE API, which deletes a resource from the server and responds back with status code 204 if the operation is completed successfully.

Feature: Delete Api Demo

  Background:
    * url 'https://reqres.in/api'
    * header Accept = 'application/json'

    Scenario: First Delete Api demo
      Given path '/users/2'
      When method DELETE
      Then status 204

In this session, we’ve seen how to automate a DELETE request in Karate. That’s all for this session. Thank you!

Karate Series | Session 4 | PUT API

What we explore in this series

Karate Basics
GET API
POST API
PUT API
DELETE API
Karate Config
Karate Reports

Similar to POST, PUT requests are used to send data to the API to create or update a resource. The main difference between POST and PUT is that PUT requests are always idempotent. That is, calling the same PUT request multiple times will always produce the same result. if you PUT an object twice, it would have no additional effect. In contrast, if you are calling a POST request back to back will make side effects of creating the same resource multiple times on the server.

Generally, whenever a PUT request creates a resource on the server, it will respond with a response code 201, and if the request modifies an existing resource, the server will return 200.

So, here we are going to see different scenarios which send requests to https://reqres.in/api/users.

Scenario 1

A simple PUT request

  #First PUT
  Scenario: Put Api demo 1
    Given path 'https://reqres.in/api/users/2'
    And request {"name": "Stanley",  "job": "Doctor"}
    When method PUT
    Then status 200
    And print response

Here, we update an existing resource on the server. We replace the existing value of a user having id 2 with a new name and job. Since the resource already exists, we will receive a response code 200. And the response would be something like this.

{
  "name": "Stanley",
  "job": "Doctor",
  "updatedAt": "2022-10-27T07:11:44.273Z"
}

Scenario 2

PUT request with background

  Background:
    * url 'https://reqres.in/api'
    * header Accept = 'application/json'

  #PUT with background
  Scenario: Put Api demo 1
    Given path '/users/2'
    And request {"name": "Stanley",  "job": "Doctor"}
    When method PUT
    Then status 200
    And print response

As we have seen on the POST call, adding a background will not make any difference in the behavior of the request. It will work as before and send back a similar response.

So we’ve seen how to automate a PUT request with Karate with and without background. That’s all for this session. Here’s the full feature file for your reference.

Feature: Put Api Demo

  Background:
    * url 'https://reqres.in/api'
    * header Accept = 'application/json'

  #First PUT
  Scenario: Put Api demo 1
    Given path '/users/2'
    And request {"name": "Stanley",  "job": "Doctor"}
    When method PUT
    Then status 200
    And print response

In this session, we’ve seen how to automate a PUT request in Karate. That’s all for this session. Thank you!

Karate Series | Session 3 | POST API

What we explore in this series

Karate Basics
GET API
POST API
PUT API
DELETE API
Karate Config
Karate Reports

In web services, POST requests are used to send data to the API server to create a resource. The data sent to the server is stored in the request body of the HTTP request. The simplest example is a Contact Us form on any website.

So, here we are going to see different scenarios which send requests to https://reqres.in/api/users.

Scenario 1

A simple POST request

Feature: Post Api feature

  Scenario: Post API demo 1
    Given url 'https://reqres.in/api/users'
    And request {"first_name":"Aidan", "last_name": "Stanley"}
    When method POST
    Then status 201
    And print response

Like what we have seen in the GET API post, this scenario is pretty straightforward. We define a URL, create the payload (which is in JSON format), define the method, and expect the status code 201. Finally, we print the response also, and the sample output is given below.

{
  "createdAt": "2022-09-29T09:56:46.268Z",
  "last_name": "Stanley",
  "id": "281",
  "first_name": "Aidan"
}

Scenario 2

POST call with Background

Feature: Post Api feature

  Background:
    * url 'https://reqres.in/api/'
    * header Accept = 'application/json'

  Scenario: Post API demo 2
    Given path '/users'
    And request {"first_name":"Peter", "last_name": "Clark"}
    When method POST
    Then status 201
    And print response

It’s pretty similar to what we did for the GET call with Background in our previous post. Here, the URL is 'https://reqres.in/api/' and the path given is '/users', so combining both we will get our actual endpoint. When running this scenario, we can clearly observe that Scenario 1 and Scenario 2 behave exactly in the same way and produce a similar response.

Scenario 3

Verify the response with Assertions

Feature: Post Api feature

  Background:
    * url 'https://reqres.in/api/'
    * header Accept = 'application/json'
  
  Scenario: Post API demo 3
    Given path '/users'
    And request {"first_name":"Alexis", "last_name": "Stuart"}
    When method POST
    Then status 201
    And match response ==  {"createdAt": "#ignore",  "last_name": "Stuart",  "id": "#string",  "first_name": "#present"}

As we have already seen, the POST request also sends back the response, so that we can easily check if we have succeeded in making the request.

Here, we are not interested in the createdAt property, that’s why we have just ignored it using the #ignore keyword. We also assert the id property is of type ‘string‘ and make sure the first_name property is present in the response.

Scenario 4

Working with files

Now, we are going to define the request object and save it in a file namely request.json. It’s being saved under /src/test/java/resources directory.

{
  "first_name": "Alexis",
  "last_name": "Stuart"
}

Now, we are going to create another file for response. It’s named response.json and saved under /src/test/java/resources directory.

{
  "createdAt": "#ignore",
  "last_name": "Stuart",
  "id": "#string",
  "first_name": "Alexis"
}

What we are going to do here is, send request.json as payload and once we get the response, we match it against response.json. So we wanted to read these files first and do the rest. Luckily Karate has a built-in read() method to read from the files.

Feature: Post Api feature

  Background:
    * url 'https://reqres.in/api/'
    * header Accept = 'application/json'
    * def projectPath = karate.properties['user.dir']
    * def resourcePath = projectPath + '/src/test/java/resources'
    * def requestBody = read(resourcePath + "/request.json")
    * def responseData = read(resourcePath + "/response.json")

  Scenario: Post API demo 4
    Given path '/users'
    And request requestBody
    When method POST
    Then status 201
    And match response ==  responseData

So, here we define global variables in the Background, and as we have already seen, the Background will be executed first before the Scenario is executed. The projectPath will be the absolute path to your project (D:\Applications\Karate Demo). From the projectPath, we create resourcePath (D:\Applications\Karate Demo\src\test\java\resources). Then, we read the request and the response files, which were already present in the resourcePath.

Scenario 5

Change property values before sending

Now, we know how to read from JSON files and use those data in scenarios. Here we read from request.json, make some changes to the data and send it along with the request as a payload. On the response, we match it with the data sent.

Feature: Post Api feature

  Background:
    * url 'https://reqres.in/api/'
    * header Accept = 'application/json'
    * def projectPath = karate.properties['user.dir']
    * def resourcePath = projectPath + '/src/test/java/resources'
    * def requestBody = read(resourcePath + "/request.json")
    * def responseData = read(resourcePath + "/response.json")

  Scenario: Post API demo 5
    Given path '/users'
    And def reqBody = requestBody
    And set reqBody.first_name = 'Craig'
    And print reqBody
    And request reqBody
    When method POST
    Then status 201
    And print response
    And assert response != null
    And assert response.first_name == 'Craig'

In the original request file, the first name was ‘Alexis’. Now we change it to ‘Craig’ and send the request. Once we get the response, we verify if the value of the first_name property is ‘Craig’

Here’s the full feature file

Feature: Post Api feature

  Background:
    * url 'https://reqres.in/api/'
    * header Accept = 'application/json'
    * def projectPath = karate.properties['user.dir']
    * def resourcePath = projectPath + '/src/test/java/resources'
    * def requestBody = read(resourcePath + "/request.json")
    * def responseData = read(resourcePath + "/response.json")

  #First POST
  Scenario: Post API demo 1
    Given url 'https://reqres.in/api/users'
    And request {"first_name":"Aidan", "last_name": "Stanley"}
    When method POST
    Then status 201
    And print response

  #With background
  Scenario: Post API demo 2
    Given path '/users'
    And request {"first_name":"Peter", "last_name": "Clark"}
    When method POST
    Then status 201
    And print response

  #With assertions
  Scenario: Post API demo 3
    Given path '/users'
    And request {"first_name":"Alexis", "last_name": "Stuart"}
    When method POST
    Then status 201
    And match response ==  {"createdAt": "#ignore",  "last_name": "Stuart",  "id": "#string",  "first_name": "#present"}

  #With request and response files
  Scenario: Post API demo 4
    Given path '/users'
    And request requestBody
    When method POST
    Then status 201
    And match response ==  responseData

  #Change request file properties
  Scenario: Post API demo 5
    Given path '/users'
    And def reqBody = requestBody
    And set reqBody.first_name = 'Craig'
    And print reqBody
    And request reqBody
    When method POST
    Then status 201
    And print response
    And assert response != null
    And assert response.first_name == 'Craig'

In this session, we’ve seen how to automate a POST request in Karate. That’s all for this session. Thank you!