google-site-verification: google61fe8ba583a51912.html
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