← Back to DevBytes

Karate: Complete Testing Guide for Developers

Karate: A Complete Testing Guide for Developers

Karate is an open-source unified testing framework that combines API testing, mocking, performance testing, and UI automation into a single, cohesive toolset. Originally designed for HTTP API testing, Karate has evolved to support a wide range of protocols including gRPC, GraphQL, SOAP, and even browser automation through WebDriver integration. Unlike traditional testing frameworks that require extensive programming knowledge, Karate uses a domain-specific language (DSL) written in plain text Gherkin-style syntax, making tests readable, maintainable, and accessible to both technical and non-technical team members.

What Makes Karate Different

Traditional API testing tools like Postman, REST-assured, or Supertest require developers to write significant amounts of boilerplate code for assertions, JSON parsing, and HTTP request construction. Karate eliminates this overhead by providing built-in capabilities for:

Core Architecture

Karate's architecture is built on a Java-based test runner that interprets feature files written in Gherkin syntax. Each feature file contains scenarios that describe test steps using Karate's expressive DSL. The framework handles all the underlying complexity of HTTP communication, JSON manipulation, and assertion logic, allowing developers to focus on what they're testing rather than how they're testing it.

The key components of Karate's architecture include:

Why Karate Matters for Modern Development

🚀 Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

Reducing Test Maintenance Burden

In microservices architectures, the number of API endpoints multiplies rapidly. Each endpoint requires tests, and traditional approaches lead to massive codebases of test utilities, helpers, and assertion libraries. Karate collapses this complexity by providing a declarative syntax where HTTP requests, response validation, and assertions coexist in a single readable file. A typical Karate test for a REST endpoint requires 5-10 lines of DSL compared to 30-50 lines in REST-assured or equivalent Java code.

Unified Testing Stack

Most organizations maintain separate tools for API testing, performance testing, and UI automation. This fragmentation creates knowledge silos, inconsistent reporting, and duplicated effort. Karate provides a single framework that handles all three domains, allowing teams to share test scenarios, reuse request patterns, and maintain a single reporting pipeline. The same feature file that validates an API response can be extended to include performance assertions or trigger UI validations.

Built-in Parallel Execution

Karate includes native support for parallel test execution without requiring external test runners or complex configuration. By simply setting the thread count in the runner class, developers can execute hundreds of API tests simultaneously. This is particularly valuable in CI/CD pipelines where fast feedback is critical. The framework handles thread safety automatically, ensuring that cookies, headers, and authentication tokens are properly isolated per thread.

Service Virtualization and Mocking

Karate's mocking capabilities allow teams to simulate dependent services with minimal effort. Instead of maintaining separate mock servers or complex WireMock configurations, developers can create mock feature files that define request matching rules and response templates. These mocks can run as standalone servers or be embedded within test suites, enabling true isolated testing of microservices.

Getting Started with Karate

Project Setup

For a Maven project, add the following dependency to your pom.xml:

<dependency>
    <groupId>com.intuit.karate</groupId>
    <artifactId>karate-core</artifactId>
    <version>1.5.1</version>
    <scope>test</scope>
</dependency>

<dependency>
    <groupId>com.intuit.karate</groupId>
    <artifactId>karate-junit5</artifactId>
    <version>1.5.1</version>
    <scope>test</scope>
</dependency>

For Gradle, add to build.gradle:

testImplementation 'com.intuit.karate:karate-core:1.5.1'
testImplementation 'com.intuit.karate:karate-junit5:1.5.1'

Project Structure Convention

Karate follows a convention-based structure that makes test organization straightforward:

src/
  test/
    java/
      examples/
        ExamplesTest.java          # Test runner
    resources/
      examples/
        example.feature            # Feature file
      karate-config.js             # Global configuration

Feature files live in the resources directory alongside their corresponding Java runner classes. The karate-config.js file in the resources root provides global configuration that applies to all tests.

Writing Your First Test

Create a feature file named first-test.feature in your test resources:

Feature: Sample API Tests

  Background:
    * url 'https://jsonplaceholder.typicode.com'

  Scenario: Get a single post and validate response
    Given path '/posts/1'
    When method GET
    Then status 200
    And match response == { id: 1, userId: '#number', title: '#string', body: '#string' }
    And match response.id == 1
    And match response.title contains 'sunt'

  Scenario: Create a new resource with a JSON body
    Given path '/posts'
    And request {
      title: 'Karate Test Post',
      body: 'This is a test body',
      userId: 1
    }
    When method POST
    Then status 201
    And match response contains { id: '#number', title: 'Karate Test Post' }

Create a JUnit 5 test runner class:

package examples;

import com.intuit.karate.junit5.Karate;

public class FirstTestRunner {
    
    @Karate.Test
    Karate testAll() {
        return Karate.run("first-test").relativeTo(getClass());
    }
}

Karate Configuration File

The karate-config.js file allows you to define reusable variables, environment-specific settings, and global hooks:

function fn() {
    var env = karate.env || 'dev';
    var config = {
        baseUrl: 'https://jsonplaceholder.typicode.com',
        timeout: 5000,
        retryCount: 3
    };
    
    if (env === 'staging') {
        config.baseUrl = 'https://staging-api.example.com';
        config.timeout = 10000;
    }
    
    if (env === 'production') {
        config.baseUrl = 'https://api.example.com';
        config.timeout = 15000;
        config.retryCount = 5;
    }
    
    karate.configure('connectTimeout', 5000);
    karate.configure('readTimeout', config.timeout);
    
    return config;
}

The configuration function runs before each scenario, and the returned object's properties become available as variables in all feature files. The karate.env property can be set via system property or command line argument to switch between environments.

Core Testing Capabilities

HTTP Request Handling

Karate provides a fluent DSL for constructing HTTP requests with full control over headers, query parameters, form fields, and request bodies:

Scenario: Complete HTTP request demonstration
    Given url 'https://httpbin.org/anything'
    And headers { Authorization: 'Bearer token123', Accept: 'application/json' }
    And param page = 1
    And param limit = 10
    And request {
        name: 'John Doe',
        email: 'john@example.com',
        preferences: {
            theme: 'dark',
            notifications: true
        }
    }
    When method POST
    Then status 200
    And match response.json.name == 'John Doe'

Response Validation and Matching

Karate's matching engine is one of its most powerful features. It supports exact matching, partial matching, fuzzy matching with placeholders, and schema validation:

Scenario: Advanced response matching techniques
    Given url 'https://jsonplaceholder.typicode.com/users/1'
    When method GET
    Then status 200
    
    # Exact match
    And match response.id == 1
    
    # Type placeholders - validates type but not value
    And match response == { 
        id: '#number', 
        name: '#string', 
        email: '#string', 
        address: '#object',
        company: '#object'
    }
    
    # Partial match - only checks specified fields
    And match response contains {
        username: 'Bret',
        email: '#string',
        address: { city: '#string', zipcode: '#string' }
    }
    
    # Array matching with each
    And match response.address.geo.lat == '#number'
    
    # Null checking
    And match response.website != null

JSON Schema Validation

Karate supports JSON Schema validation natively. You can define schemas inline or reference external schema files:

Scenario: JSON Schema validation
    Given url 'https://jsonplaceholder.typicode.com/users/1'
    When method GET
    Then status 200
    
    # Inline schema validation
    And match response == """
    {
      "type": "object",
      "properties": {
        "id": { "type": "integer" },
        "name": { "type": "string" },
        "email": { "type": "string", "format": "email" },
        "address": {
          "type": "object",
          "properties": {
            "street": { "type": "string" },
            "city": { "type": "string" }
          },
          "required": ["street", "city"]
        }
      },
      "required": ["id", "name", "email"]
    }
    """

Reusing Requests with Call and Karate Expressions

Karate enables you to define reusable JavaScript functions for common operations and call other feature files as subroutines:

Feature: Reusable authentication module

Scenario: Generate authentication token
    Given url authServiceUrl
    And request { username: username, password: password }
    When method POST
    Then status 200
    And def authToken = response.token

To call this reusable scenario from another feature file:

Scenario: Use shared authentication
    * def authResult = call read('classpath:auth/login.feature') { username: 'testuser', password: 'testpass' }
    * def authToken = authResult.authToken
    
    Given url apiBaseUrl
    And headers { Authorization: 'Bearer ' + authToken }
    Given path '/protected-resource'
    When method GET
    Then status 200

Data-Driven Testing with Examples

Karate supports data-driven testing through scenario outlines with examples tables:

Scenario Outline: Validate multiple user accounts
    Given url 'https://jsonplaceholder.typicode.com'
    Given path '/users/' + userId
    When method GET
    Then status 200
    And match response.id == userId
    And match response.email == expectedEmail
    
    Examples:
    | userId | expectedEmail            |
    | 1      | 'Sincere@april.biz'     |
    | 2      | 'Shanna@melissa.tv'     |
    | 3      | 'Nathan@yesenia.net'    |
    | 4      | 'Julianne.OConner@kory.org' |

Handling Complex JSON Transformations

Karate's JavaScript engine allows powerful data transformations between API calls:

Scenario: Extract and transform data between requests
    Given url 'https://jsonplaceholder.typicode.com/users'
    When method GET
    Then status 200
    
    # Extract specific fields into a new array
    * def userSummaries = []
    * def extractSummary = function(user) {
        return { name: user.name, city: user.address.city }
    }
    * def userSummaries = karate.map(response, extractSummary)
    
    # Use transformed data in subsequent request
    Given url 'https://httpbin.org/anything'
    And request { summaries: userSummaries, count: response.length }
    When method POST
    Then status 200

Mocking and Service Virtualization

Creating Mock Services

Karate allows you to create mock servers from feature files. These mocks can match incoming requests based on path, method, headers, and body content, and return predefined responses:

Feature: Mock user service

Background:
    * configure mock = true

Scenario: Mock GET user by ID
    * def expectedId = request.pathParams.id
    When method GET
    Then match request.method == 'GET'
    And match request.headers['Authorization'] == 'Bearer mock-token'
    * def response = """
    {
        "id": "#(expectedId)",
        "name": "Mock User",
        "email": "mock@example.com",
        "status": "active"
    }
    """
    * def responseStatus = 200

Scenario: Mock user creation
    When method POST
    Then match request.body contains { username: '#string' }
    * def response = """
    {
        "id": 999,
        "username": "#(request.body.username)",
        "created": true,
        "timestamp": "2024-01-01T00:00:00Z"
    }
    """
    * def responseStatus = 201

Scenario: Mock user not found
    Given path '/users/999'
    When method GET
    Then match request.method == 'GET'
    * def response = { error: 'User not found', code: 404 }
    * def responseStatus = 404

To start the mock server programmatically:

package examples;

import com.intuit.karate.MockServer;
import com.intuit.karate.junit5.Karate;

public class MockServerRunner {
    
    public static void main(String[] args) {
        MockServer server = MockServer
            .feature("classpath:mocks/user-mock.feature")
            .http(8080)
            .build();
        server.start();
        System.out.println("Mock server started on port 8080");
    }
}

Using Mocks in Tests

Mocks can be started inline within test scenarios, making integration tests self-contained:

Scenario: Test with embedded mock server
    * def mockServer = karate.start('classpath:mocks/payment-gateway-mock.feature')
    * def paymentServiceUrl = 'http://localhost:' + mockServer.port
    
    Given url paymentServiceUrl
    Given path '/process-payment'
    And request { amount: 100, currency: 'USD', cardToken: 'tok_visa' }
    When method POST
    Then status 200
    And match response.status == 'succeeded'
    
    * karate.stop(mockServer.port)

Performance Testing with Gatling

Karate-Gatling Integration

Karate integrates seamlessly with Gatling, a powerful performance testing tool. Feature files can be reused as performance test scenarios without modification:

package examples;

import com.intuit.karate.gatling.PreDef;
import io.gatling.javaapi.core.*;
import io.gatling.javaapi.core.CoreDsl.*;

import static io.gatling.javaapi.core.CoreDsl.*;
import static io.gatling.javaapi.http.HttpDsl.*;

public class PerformanceSimulation extends Simulation {
    
    {
        HttpProtocolBuilder httpProtocol = http
            .baseUrl("https://api.example.com")
            .acceptHeader("application/json");
        
        ScenarioBuilder apiLoadTest = scenario("API Load Test")
            .exec(karateFeature("classpath:examples/api-tests.feature"));
        
        setUp(
            apiLoadTest.injectOpen(
                rampUsers(50).during(Duration.ofSeconds(30)),
                constantUsersPerSec(10).during(Duration.ofMinutes(2))
            )
        ).protocols(httpProtocol);
    }
}

UI Automation

Browser Testing with Karate

Karate's UI automation module leverages WebDriver and Chrome DevTools Protocol for browser testing. Tests are written in the same declarative style as API tests:

Feature: Web application login test

Background:
    * configure driver = { type: 'chrome', headless: false }
    * driver 'https://example.com/login'

Scenario: Successful login flow
    Given driver.input('#username', 'testuser@example.com')
    And driver.input('#password', 'SecurePass123!')
    When driver.click('#login-button')
    Then driver.waitFor('#dashboard-container', 5000)
    And driver.text('#welcome-message') contains 'Welcome back'
    And driver.exists('#user-profile-menu')
    
    # Validate API call triggered by login
    * driver.intercept = true
    * driver.click('#logout-button')
    And match driver.interceptedRequests contains { url: '#string', method: 'POST' }

Hybrid API and UI Testing

One of Karate's most powerful capabilities is combining API and UI tests within the same scenario. This enables end-to-end validation where API calls set up test data and UI tests validate the user experience:

Scenario: End-to-end order placement
    # Step 1: Create order via API
    Given url 'https://api.example.com'
    Given path '/orders'
    And request {
        items: [{ productId: 'SKU123', quantity: 2 }],
        shippingAddress: { street: '123 Main St', city: 'Testville' }
    }
    When method POST
    Then status 201
    * def orderId = response.orderId
    
    # Step 2: Verify order appears in UI
    * driver 'https://app.example.com/orders/' + orderId
    And driver.waitFor('.order-details', 5000)
    And driver.text('.order-status') == 'Created'
    And driver.text('.order-id') == orderId
    
    # Step 3: Update order status via API
    Given path '/orders/' + orderId + '/status'
    And request { status: 'shipped', trackingNumber: 'TRK789' }
    When method PUT
    Then status 200
    
    # Step 4: Verify status reflected in UI
    * driver.refresh()
    And driver.waitFor('.order-status.shipped', 5000)
    And driver.text('.tracking-number') == 'TRK789'

Advanced Features and Patterns

Conditional Logic and Dynamic Scenarios

Karate's JavaScript engine enables conditional test execution and dynamic scenario generation:

Scenario: Conditional API validation based on user role
    Given url 'https://api.example.com'
    Given path '/users/me'
    When method GET
    Then status 200
    
    * def validateAdminFields = function() {
        karate.log('Validating admin-specific fields');
        match response.permissions != null;
        match response.permissions.length > 0;
        match response.adminSince == '#string';
    }
    
    * def validateUserFields = function() {
        karate.log('Validating standard user fields');
        match response.preferences == '#object';
    }
    
    # Execute different validations based on role
    * if (response.role == 'admin') karate.call(validateAdminFields)
    * if (response.role != 'admin') karate.call(validateUserFields)

Retry Logic and Resilience Testing

Karate provides built-in retry capabilities for handling flaky services and testing resilience patterns:

Scenario: Retry on transient failures
    * configure retry = { count: 3, interval: 2000 }
    
    Given url 'https://api.example.com/health'
    When method GET
    Then status 200
    And match response.status == 'healthy'
    
    # Retry with custom condition
    * def isValid = function(response) {
        return response.status == 200 && response.json.status == 'ready'
    }
    * retry until isValid

GraphQL Testing

Karate supports GraphQL APIs with the same declarative syntax used for REST:

Scenario: GraphQL query with variables
    Given url 'https://graphql.example.com/graphql'
    And headers { Authorization: 'Bearer ' + authToken }
    And request """
    {
      "query": "query GetUser($id: ID!) { user(id: $id) { name email posts { title } } }",
      "variables": { "id": "123" }
    }
    """
    When method POST
    Then status 200
    And match response.data.user.name == '#string'
    And match response.data.user.posts == '#array'
    And match each response.data.user.posts contains { title: '#string' }

SOAP and XML Testing

Karate handles XML-based SOAP services naturally:

Scenario: SOAP service invocation
    Given url 'https://soap.example.com/WeatherService'
    And headers { SOAPAction: 'GetWeather' }
    And request """
    
        
            
                NYC
                2024-12-25
            
        
    
    """
    When method POST
    Then status 200
    And match /Envelope/Body/GetWeatherResponse/Temperature == '#number'
    And match /Envelope/Body/GetWeatherResponse/Conditions contains 'Sunny'

Best Practices for Karate Testing

Organize Feature Files by Domain

Structure feature files around business domains rather than technical endpoints. Group related scenarios in feature files that represent complete workflows:

src/test/resources/
  auth/
    login.feature
    registration.feature
    password-reset.feature
  orders/
    create-order.feature
    order-lifecycle.feature
    order-search.feature
  users/
    user-profile.feature
    user-preferences.feature
  shared/
    auth-helper.feature
    data-cleanup.feature

Leverage the Background Section

Use the Background section to set up common prerequisites that apply to all scenarios in a feature file. This reduces repetition and improves maintainability:

Feature: Order Management API

Background:
    * url baseUrl
    * configure headers = { Authorization: 'Bearer ' + authToken }
    * def authToken = call read('classpath:shared/auth-helper.feature')
    * configure retry = { count: 2, interval: 1000 }

Implement Proper Environment Management

Create a comprehensive karate-config.js that handles multiple environments, secrets management, and dynamic configuration:

function fn() {
    var env = karate.env || 'local';
    var config = {};
    
    // Base configuration
    config.timeout = 5000;
    config.retryCount = 3;
    
    // Environment-specific overrides
    if (env === 'local') {
        config.baseUrl = 'http://localhost:8080';
        config.authUrl = 'http://localhost:8081';
        config.mockExternalServices = true;
    } else if (env === 'ci') {
        config.baseUrl = 'http://test-container:8080';
        config.authUrl = 'http://auth-container:8081';
        config.mockExternalServices = true;
    } else if (env === 'staging') {
        config.baseUrl = 'https://staging-api.example.com';
        config.authUrl = 'https://staging-auth.example.com';
        config.mockExternalServices = false;
    }
    
    // Secrets from environment variables
    config.apiKey = karate.properties['api.key'] || java.lang.System.getenv('API_KEY');
    
    return config;
}

Implement Custom Reporting

Enhance Karate's built-in reporting with custom hooks and listeners:

package utils;

import com.intuit.karate.RuntimeHook;
import com.intuit.karate.Suite;

public class CustomReportHook implements RuntimeHook {
    
    @Override
    public boolean beforeScenario(Scenario scenario) {
        scenario.getTags().forEach(tag -> {
            if (tag.equals("critical")) {
                scenario.getLogger().info("Starting critical scenario: " + scenario.getName());
            }
        });
        return true;
    }
    
    @Override
    public void afterScenario(Scenario scenario) {
        if (scenario.isFailed()) {
            scenario.getLogger().error("Scenario failed: " + scenario.getName());
            // Trigger alert or notification
            sendFailureNotification(scenario);
        }
    }
}

Handle Authentication Patterns

Centralize authentication logic in reusable feature files to avoid duplicating login steps across tests:

Feature: OAuth2 authentication helper

Scenario: Obtain access token via client credentials
    Given url authUrl
    Given path '/oauth/token'
    And headers { Content-Type: 'application/x-www-form-urlencoded' }
    And form field grant_type = 'client_credentials'
    And form field client_id = clientId
    And form field client_secret = clientSecret
    When method POST
    Then status 200
    * def accessToken = response.access_token
    * def tokenType = response.token_type
    
Scenario: Obtain token via password grant
    Given url authUrl
    Given path '/oauth/token'
    And form field grant_type = 'password'
    And form field username = username
    And form field password = password
    When method POST
    Then status 200
    * def accessToken = response.access_token

Use Tags for Test Filtering

Apply tags to scenarios for selective test execution in different pipeline stages:

@smoke @critical @api
Feature: Critical payment endpoints

@smoke @positive
Scenario: Successful payment processing
    Given url paymentServiceUrl
    # ...

@integration @slow
Scenario: End-to-end payment with fraud detection
    Given url paymentServiceUrl
    # ...

@negative @edge-case
Scenario: Payment with expired card
    Given url paymentServiceUrl
    # ...

Run specific tag subsets from the runner:

* def result = Karate.run('classpath:payments/').tags('@smoke').parallel(5)

Manage Test Data Carefully

Use Karate's embedded expressions and JavaScript functions to generate dynamic test data rather than hardcoding values:

Scenario: Create user with dynamic data
    * def randomSuffix = Math.floor(Math.random() * 10000)
    * def testEmail = 'user_' + randomSuffix + '@example.com'
    * def testUsername = 'testuser_' + randomSuffix
    
    Given url baseUrl
    Given path '/users'
    And request {
        username: testUsername,
        email: testEmail,
        name: 'Test User ' + randomSuffix,
        metadata: {
            createdAt: karate.timestamp(),
            source: 'automated-test'
        }
    }
    When method POST
    Then status 201

Implement Proper Cleanup

Design tests to clean up after themselves, especially in shared environments:

Feature: Resource lifecycle management

Background:
    * url baseUrl
    * def createdResources = []

Scenario: Create and verify resource
    Given path '/resources'
    And request { name: 'test-resource', type: 'temporary' }
    When method POST
    Then status 201
    * def resourceId = response.id
    * createdResources.push(resourceId)
    
    # Verify resource
    Given path '/resources/' + resourceId
    When method GET
    Then status 200
    
    # Cleanup
    Given path '/resources/' + resourceId
    When method DELETE
    Then status 204

Continuous Integration Integration

Maven Surefire Configuration

Configure Maven to run Karate tests during the integration-test phase:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>3.2.5</version>
    <configuration>
        <includes>
            <include>**/*Test.java</include>
            <include>**/*Runner.java</include>
        </includes>
        <systemPropertyVariables>
            <karate.env>ci</karate.env>
        </systemPropertyVariables>
    </configuration>
</plugin>

Jenkins Pipeline Integration

pipeline {
    agent any
    
    stages {
        stage('Karate API Tests') {
            steps {
                sh 'mvn test -Dkarate.env=ci -Dtest=TestParallel'
            }
            post {
                always {
                    publishHTML(
                        target: [
                            allowMissing: false,
                            always: true,
                            reportDir: 'target/karate-reports',
                            reportName: 'Karate Test Report'
                        ]
                    )
                }
            }
        }
    }
}

Debugging and Troubleshooting

Enabling Detailed Logging

Karate provides comprehensive logging options for debugging test failures:

# In karate-config.js
karate.configure('logPrettyRequest', true);
karate.configure('logPrettyResponse', true);

# Or per scenario
Scenario: Debug with verbose logging
    * configure logPrettyRequest = true
    * configure logPrettyResponse = true
    Given url 'https://api.example.com/endpoint'
    When method GET
    Then status 200

Using the Karate IDE Debugger

For complex scenarios, use the Karate IntelliJ plugin or VS Code extension to set breakpoints and step through feature files line by line. The debugger shows variable values, request/response bodies, and allows expression evaluation at each step.

Common Issues and Solutions

Conclusion

Karate represents a significant evolution in how development teams approach testing. By unifying API testing, mocking, performance testing, and UI automation under a single declarative framework, it eliminates the traditional fragmentation that slows down delivery pipelines. The framework's design philosophy—prioritizing readability, reducing boilerplate, and enabling parallel execution—aligns perfectly with modern microservices architectures and continuous delivery requirements.

Teams adopting Karate consistently report reduced test maintenance overhead, faster test creation cycles, and broader test coverage across their service ecosystems. The ability to reuse the same feature files for functional validation, performance benchmarking, and integration testing creates a single source of truth for service behavior expectations. As software systems continue to grow in complexity, tools like Karate that simplify testing while expanding capability will become increasingly essential to maintaining delivery velocity and system reliability.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles