Behavior-Driven API Testing: Mastering Karate DSL in Java

Article

Karate DSL is a powerful open-source testing framework that combines API test automation, performance testing, and mocks into a single, unified tool. Unlike traditional testing frameworks, Karate uses a behavior-driven development (BDD) syntax that allows even non-technical stakeholders to understand tests, while providing powerful capabilities for Java developers. This article explores how to leverage Karate DSL for comprehensive API testing in Java projects.


Why Karate DSL?

Key Advantages:

  • No Java Code Required: Write tests in Gherkin-like syntax
  • Built-in Assertions: Comprehensive validation capabilities
  • Test Doubles: Built-in mock server for testing
  • Performance Testing: Integration with Gatling for load testing
  • Parallel Execution: Run tests in parallel for faster feedback
  • JSON/XML Support: First-class support for modern data formats

Setting Up Karate DSL

1. Maven Dependencies

<properties>
<karate.version>1.4.1</karate.version>
<maven-surefire.version>3.1.2</maven-surefire.version>
</properties>
<dependencies>
<!-- Karate Core -->
<dependency>
<groupId>com.intuit.karate</groupId>
<artifactId>karate-core</artifactId>
<version>${karate.version}</version>
<scope>test</scope>
</dependency>
<!-- Karate JUnit 5 -->
<dependency>
<groupId>com.intuit.karate</groupId>
<artifactId>karate-junit5</artifactId>
<version>${karate.version}</version>
<scope>test</scope>
</dependency>
<!-- Apache HTTP Client for advanced HTTP features -->
<dependency>
<groupId>com.intuit.karate</groupId>
<artifactId>karate-apache</artifactId>
<version>${karate.version}</version>
<scope>test</scope>
</dependency>
<!-- Test Containers for integration testing -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>1.19.3</version>
<scope>test</scope>
</dependency>
<!-- AssertJ for additional assertions -->
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.24.2</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<testResources>
<testResource>
<directory>src/test/java</directory>
<excludes>
<exclude>**/*.java</exclude>
</excludes>
</testResource>
<testResource>
<directory>src/test/resources</directory>
</testResource>
</testResources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${maven-surefire.version}</version>
<configuration>
<includes>
<include>**/*Test.java</include>
<include>**/*TestRunner.java</include>
</includes>
<systemPropertyVariables>
<karate.options>--tags ~@ignore</karate.options>
<karate.env>${karate.env:-dev}</karate.env>
</systemPropertyVariables>
</configuration>
</plugin>
</plugins>
</build>

2. Project Structure

src/test/java
├── com/example/api/
│   ├── runners/
│   │   ├── ApiTestRunner.java
│   │   ├── UserApiTestRunner.java
│   │   └── ParallelTestRunner.java
│   └── helpers/
│       ├── JavaHelpers.java
│       └── TestDataGenerator.java
└── com/example/api/features/
├── users/
│   ├── users.feature
│   ├── user-management.feature
│   └── user-search.feature
├── products/
│   ├── products.feature
│   └── product-categories.feature
├── orders/
│   └── orders.feature
├── common/
│   ├── common.feature
│   └── auth.feature
└── utils/
├── config.js
├── utils.js
└── test-data.js

Core Karate Features

1. Basic API Test Example

# src/test/java/com/example/api/features/users/users.feature
Feature: User API CRUD Operations
Background:
* url baseUrl
* configure headers = { 'Content-Type': 'application/json', 'X-API-Key': '#(apiKey)' }
* def createUserRequest = read('classpath:com/example/api/features/users/create-user-request.json')
* def updateUserRequest = read('classpath:com/example/api/features/users/update-user-request.json')
Scenario: Create a new user successfully
Given path 'users'
And request createUserRequest
When method post
Then status 201
And match response ==
"""
{
id: '#number',
firstName: '#(createUserRequest.firstName)',
lastName: '#(createUserRequest.lastName)',
email: '#(createUserRequest.email)',
status: 'ACTIVE',
createdAt: '#notnull',
updatedAt: '#notnull'
}
"""
And def userId = response.id
And print 'Created user with ID:', userId
Scenario: Get user by ID
Given path 'users', userId
When method get
Then status 200
And match response.id == userId
And match response.status == 'ACTIVE'
Scenario: Update user information
Given path 'users', userId
And request updateUserRequest
When method put
Then status 200
And match response.firstName == updateUserRequest.firstName
And match response.lastName == updateUserRequest.lastName
Scenario: Delete user
Given path 'users', userId
When method delete
Then status 204
Scenario: Get deleted user should return 404
Given path 'users', userId
When method get
Then status 404

2. Complex Validation with Data-Driven Testing

Feature: Advanced User API Validation with Data-Driven Tests
Background:
* url baseUrl
* configure headers = { 'Content-Type': 'application/json' }
Scenario Outline: Create users with different data combinations
Given path 'users'
And request
"""
{
"firstName": "<firstName>",
"lastName": "<lastName>", 
"email": "<email>",
"age": <age>,
"role": "<role>"
}
"""
When method post
Then status <expectedStatus>
And match response contains { id: '#number' }
And match response contains deep { role: '<role>' }
Examples:
| firstName | lastName | email               | age | role    | expectedStatus |
| John      | Doe      | [email protected]    | 25  | USER    | 201            |
| Jane      | Smith    | [email protected]    | 30  | ADMIN   | 201            |
| Bob       | Johnson  | invalid-email       | 15  | USER    | 400            |
| Alice     | Brown    | [email protected]   | 200 | USER    | 400            |
Scenario: Validate user response schema
Given path 'users'
And def userData =
"""
{
firstName: 'Schema',
lastName: 'Validator',
email: '[email protected]',
age: 28,
role: 'USER'
}
"""
And request userData
When method post
Then status 201
And match response ==
"""
{
id: '#number',
firstName: '#string',
lastName: '#string', 
email: '#regex [a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}',
age: '#number',
role: '#string',
status: '#string',
createdAt: '#notnull',
updatedAt: '#notnull'
}
"""
And match each response..role == '#? _ == "USER" || _ == "ADMIN" || _ == "MODERATOR"'
Scenario: Test user search with query parameters
Given path 'users'
And param page = 0
And param size = 10
And param sort = 'firstName,desc'
And param role = 'USER'
When method get
Then status 200
And match response.content == '#[]'
And match response.pageable ==
"""
{
pageNumber: 0,
pageSize: 10,
sort: { sorted: true, unsorted: false, empty: false }
}
"""
And match response.totalElements == '#number'
And match response.totalPages == '#number'

3. Authentication and Security Testing

Feature: API Authentication and Security
Background:
* url baseUrl
Scenario: Successful authentication with valid credentials
Given path 'auth', 'login'
And request
"""
{
"username": "testuser",
"password": "validpassword"
}
"""
When method post
Then status 200
And match response ==
"""
{
token: '#notnull',
type: 'Bearer',
expiresIn: '#number',
refreshToken: '#notnull'
}
"""
And def authToken = response.token
And configure headers = { 'Authorization': 'Bearer ' + authToken }
Scenario: Authentication with invalid credentials
Given path 'auth', 'login'
And request
"""
{
"username": "testuser",
"password": "wrongpassword"
}
"""
When method post
Then status 401
And match response ==
"""
{
error: 'UNAUTHORIZED',
message: 'Invalid credentials',
timestamp: '#notnull'
}
"""
Scenario: Access protected endpoint without token
Given path 'users', 'profile'
When method get
Then status 401
Scenario: Access protected endpoint with valid token
Given path 'users', 'profile'
And header Authorization = 'Bearer ' + authToken
When method get
Then status 200
And match response.username == 'testuser'
Scenario: Test token expiration
Given path 'auth', 'verify'
And header Authorization = 'Bearer ' + authToken
When method post
Then status 200
And match response.valid == true

4. File Upload and Download Testing

Feature: File Upload and Download Operations
Background:
* url baseUrl
* configure headers = { 'Authorization': 'Bearer ' + authToken }
Scenario: Upload user profile picture
Given path 'users', userId, 'avatar'
And multipart file file = { read: 'classpath:test-data/profile-picture.jpg', filename: 'avatar.jpg' }
And multipart field description = 'User profile picture'
When method post
Then status 200
And match response ==
"""
{
fileId: '#string',
filename: 'avatar.jpg',
size: '#number',
url: '#string',
uploadedAt: '#notnull'
}
"""
And def fileId = response.fileId
Scenario: Download uploaded file
Given path 'files', fileId
When method get
Then status 200
And match header Content-Type contains 'image/'
And match responseBytes.length > 0
Scenario: Upload file with validation
Given path 'users', userId, 'documents'
And multipart file document = { read: 'classpath:test-data/large-file.pdf', filename: 'document.pdf' }
When method post
Then status 413
And match response ==
"""
{
error: 'PAYLOAD_TOO_LARGE',
message: '#string',
maxSize: 10485760
}
"""

Advanced Karate Features

1. JavaScript Functions and Helpers

// src/test/java/com/example/api/utils/utils.js
function generateRandomEmail() {
var timestamp = new Date().getTime();
return 'testuser' + timestamp + '@example.com';
}
function generateUserData(role) {
return {
firstName: 'Test' + Math.floor(Math.random() * 1000),
lastName: 'User',
email: generateRandomEmail(),
age: Math.floor(Math.random() * 50) + 18,
role: role || 'USER'
};
}
function validateUserResponse(user) {
if (!user.id) return false;
if (!user.email.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) return false;
if (user.age < 0 || user.age > 150) return false;
return true;
}
function calculateAge(birthDate) {
var today = new Date();
var birthDate = new Date(birthDate);
var age = today.getFullYear() - birthDate.getFullYear();
var monthDiff = today.getMonth() - birthDate.getMonth();
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) {
age--;
}
return age;
}

2. Java Integration Helpers

// src/test/java/com/example/api/helpers/JavaHelpers.java
package com.example.api.helpers;
import com.intuit.karate.JsonUtils;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Base64;
import java.util.UUID;
public class JavaHelpers {
public static String generateUniqueId() {
return UUID.randomUUID().toString();
}
public static String getCurrentTimestamp() {
return LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
}
public static String encodeBase64(String data) {
return Base64.getEncoder().encodeToString(data.getBytes());
}
public static String decodeBase64(String encodedData) {
return new String(Base64.getDecoder().decode(encodedData));
}
public static boolean isValidEmail(String email) {
return email != null && email.matches("^[A-Za-z0-9+_.-]+@(.+)$");
}
public static String toJsonString(Object object) {
return JsonUtils.toJson(object);
}
public static Object fromJsonString(String json) {
return JsonUtils.toJsonDoc(json);
}
}

3. Configuration Management

// src/test/java/com/example/api/utils/config.js
function getConfig() {
var env = karate.env || 'dev';
var config = {
dev: {
baseUrl: 'http://localhost:8080/api/v1',
apiKey: 'dev-key-12345',
timeout: 5000
},
qa: {
baseUrl: 'https://qa-api.example.com/api/v1',
apiKey: 'qa-key-67890',
timeout: 10000
},
staging: {
baseUrl: 'https://staging-api.example.com/api/v1',
apiKey: 'staging-key-abcde',
timeout: 15000
},
prod: {
baseUrl: 'https://api.example.com/api/v1',
apiKey: karate.properties['api.key'] || 'default-prod-key',
timeout: 20000
}
};
var result = config[env];
if (!result) {
throw 'Invalid environment: ' + env;
}
// Set default configuration
karate.configure('connectTimeout', result.timeout);
karate.configure('readTimeout', result.timeout);
return result;
}

Test Runners and Parallel Execution

1. JUnit 5 Test Runners

// src/test/java/com/example/api/runners/ApiTestRunner.java
package com.example.api.runners;
import com.intuit.karate.junit5.Karate;
import org.junit.jupiter.api.BeforeAll;
class ApiTestRunner {
@BeforeAll
static void beforeAll() {
// Setup before all tests
System.setProperty("karate.env", System.getProperty("karate.env", "dev"));
}
@Karate.Test
Karate testAll() {
return Karate.run().relativeTo(getClass());
}
@Karate.Test
Karate testUsers() {
return Karate.run("classpath:com/example/api/features/users")
.tags("@users")
.outputCucumberJson(true);
}
@Karate.Test
Karate testProducts() {
return Karate.run("classpath:com/example/api/features/products")
.tags("@products")
.outputHtmlReport(true);
}
}
// src/test/java/com/example/api/runners/ParallelTestRunner.java
package com.example.api.runners;
import com.intuit.karate.Results;
import com.intuit.karate.Runner;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
class ParallelTestRunner {
@Test
void testParallel() {
Results results = Runner.path("classpath:com/example/api/features")
.tags("~@ignore")
.outputCucumberJson(true)
.outputJunitXml(true)
.reportDir("target/karate-reports")
.parallel(5);
assertEquals(0, results.getFailCount(), results.getErrorMessages());
}
}

2. Test Configuration with Hooks

// src/test/java/com/example/api/runners/TestBase.java
package com.example.api.runners;
import com.intuit.karate.junit5.Karate;
import org.junit.jupiter.api.BeforeAll;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.utility.DockerImageName;
public abstract class TestBase {
private static GenericContainer<?> apiContainer;
@BeforeAll
static void setup() {
String environment = System.getProperty("karate.env", "dev");
if ("local".equals(environment)) {
startTestContainers();
}
}
private static void startTestContainers() {
apiContainer = new GenericContainer<>(DockerImageName.parse("my-api:latest"))
.withExposedPorts(8080)
.withEnv("SPRING_PROFILES_ACTIVE", "test")
.withReuse(true);
apiContainer.start();
String baseUrl = "http://" + apiContainer.getHost() + ":" + apiContainer.getFirstMappedPort();
System.setProperty("karate.baseUrl", baseUrl);
}
}

Performance Testing with Gatling

1. Performance Test Feature

# src/test/java/com/example/api/features/performance/users-load.feature
Feature: User API Load Testing
@performance
Scenario: Create users under load
Given url baseUrl
And header Content-Type = 'application/json'
And header X-API-Key = '#(apiKey)'
And request
"""
{
"firstName": "LoadTest",
"lastName": "User#{id}",
"email": "loadtest#{id}@example.com",
"age": 30,
"role": "USER"
}
"""
When method post
Then status 201
@performance
Scenario: Get users under load
Given url baseUrl
And path 'users'
And param page = 0
And param size = 20
When method get
Then status 200

2. Gatling Simulation Runner

// src/test/java/com/example/api/performance/PerformanceRunner.java
package com.example.api.performance;
import com.intuit.karate.gatling.KarateProtocol;
import com.intuit.karate.gatling.PreDef.*;
import io.gatling.core.PreDef.*;
import io.gatling.core.structure.ScenarioBuilder;
import io.gatling.http.PreDef.*;
import java.time.Duration;
import java.util.Collections;
public class PerformanceRunner {
private final KarateProtocol createUserProtocol = karateProtocol()
.name("Create User")
.uriPattern("/api/v1/users");
private final KarateProtocol getUserProtocol = karateProtocol()
.name("Get Users")
.uriPattern("/api/v1/users.*");
public ScenarioBuilder createUserScenario() {
return scenario("Create User Load Test")
.exec(karateFeature("classpath:com/example/api/features/performance/users-load.feature@performance"));
}
public ScenarioBuilder getUserScenario() {
return scenario("Get Users Load Test")
.exec(karateFeature("classpath:com/example/api/features/performance/users-load.feature@performance"));
}
{
setUp(
createUserScenario().injectOpen(
rampUsersPerSec(1).to(10).during(Duration.ofMinutes(2)),
constantUsersPerSec(10).during(Duration.ofMinutes(5))
).protocols(createUserProtocol),
getUserScenario().injectOpen(
rampUsersPerSec(5).to(50).during(Duration.ofMinutes(2)),
constantUsersPerSec(50).during(Duration.ofMinutes(5))
).protocols(getUserProtocol)
).maxDuration(Duration.ofMinutes(10));
}
}

Mock Server for Testing

1. Karate Mock Server Feature

# src/test/java/com/example/api/mocks/user-service-mock.feature
Feature: User Service Mock
Background:
* configure cors = true
* configure headers = { 'Content-Type': 'application/json' }
Scenario: pathMatches('/users') && methodIs('POST')
* def response =
"""
{
"id": "#(uuid())",
"firstName": "#(request.firstName)",
"lastName": "#(request.lastName)",
"email": "#(request.email)",
"status": "ACTIVE",
"createdAt": "#(now)",
"updatedAt": "#(now)"
}
"""
* def responseStatus = 201
Scenario: pathMatches('/users/{id}') && methodIs('GET')
* def user = get[0]
* if (user) 
* def response = user
* def responseStatus = 200
* else 
* def response = { error: 'User not found' }
* def responseStatus = 404
Scenario: pathMatches('/users') && methodIs('GET')
* def users = get
* def response = 
"""
{
"content": #(users),
"pageable": {
"pageNumber": 0,
"pageSize": 20,
"sort": { "sorted": false, "unsorted": true, "empty": true }
},
"totalElements": #(users.length),
"totalPages": 1,
"last": true,
"first": true
}
"""
* def responseStatus = 200

2. Mock Server Runner

// src/test/java/com/example/api/mocks/MockServerRunner.java
package com.example.api.mocks;
import com.intuit.karate.junit5.Karate;
class MockServerRunner {
@Karate.Test
Karate testMockServer() {
return Karate.run("classpath:com/example/api/mocks/user-service-mock.feature")
.systemProperty("mock.port", "8080");
}
}

CI/CD Integration

1. Maven Surefire Configuration

<profile>
<id>karate-tests</id>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.1.2</version>
<configuration>
<includes>
<include>**/*ParallelTestRunner.java</include>
</includes>
<systemPropertyVariables>
<karate.env>${karate.env}</karate.env>
<karate.options>--tags ~@ignore</karate.options>
</systemPropertyVariables>
</configuration>
</plugin>
</plugins>
</build>
</profile>

2. GitHub Actions Workflow

# .github/workflows/api-tests.yml
name: API Tests
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
api-tests:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:14
env:
POSTGRES_PASSWORD: test
POSTGRES_DB: testdb
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- uses: actions/checkout@v3
- name: Set up JDK 21
uses: actions/setup-java@v3
with:
java-version: '21'
distribution: 'temurin'
cache: 'maven'
- name: Run API Tests
run: mvn test -Pkarate-tests -Dkarate.env=ci
env:
API_BASE_URL: http://localhost:8080
DATABASE_URL: jdbc:postgresql://localhost:5432/testdb
- name: Upload Test Reports
uses: actions/upload-artifact@v3
with:
name: karate-reports
path: target/karate-reports/
retention-days: 7

Best Practices

1. Test Organization

  • Group features by business domain
  • Use descriptive scenario names
  • Implement proper tagging strategy
  • Separate positive and negative tests

2. Data Management

  • Use dynamic test data generation
  • Clean up test data after execution
  • Implement data factories for complex objects
  • Use scenario outlines for data-driven testing

3. Maintenance

  • Regular review of test patterns
  • Update tests for API changes
  • Monitor test execution time
  • Refactor common steps into background

Conclusion

Karate DSL provides a comprehensive solution for API testing that combines simplicity with powerful features. Key benefits include:

  • Reduced Learning Curve: BDD syntax is accessible to all team members
  • Comprehensive Testing: Supports REST, SOAP, GraphQL, and more
  • Built-in Capabilities: No need for additional assertion libraries
  • Performance Testing: Integrated Gatling support
  • Mock Server: Built-in test doubles for integration testing
  • Parallel Execution: Fast test execution with minimal configuration

By leveraging Karate DSL, teams can achieve high test coverage with maintainable, readable tests that provide fast feedback and reliable API validation across the entire development lifecycle.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper