Dredd for API Blueprint in Java: API Contract Testing

Dredd is a language-agnostic tool for testing API documentation against the actual API implementation. It validates that your API Blueprint documentation accurately describes your API's behavior.


Core Concepts

What is Dredd?

  • Command-line tool for API contract testing
  • Validates API implementation against API Blueprint documentation
  • Supports multiple API description formats (API Blueprint, OpenAPI)
  • Language-agnostic - works with any API

Why Use Dredd?

  • Contract Testing: Ensure API matches documentation
  • Continuous Validation: Catch breaking changes early
  • Documentation Accuracy: Keep docs in sync with implementation
  • Regression Testing: Prevent API regressions

Setup and Dependencies

1. Prerequisites Installation
# Install Dredd globally
npm install -g dredd
# Or use npx (no installation required)
npx dredd --version
# Install API Blueprint parser (optional)
npm install -g aglio
2. Maven Dependencies for Spring Boot API
<properties>
<spring-boot.version>3.1.0</spring-boot.version>
<rest-assured.version>5.3.0</rest-assured.version>
<junit.version>5.9.2</junit.version>
</properties>
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<!-- Spring Boot Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>${spring-boot.version}</version>
<scope>test</scope>
</dependency>
<!-- REST Assured for API Testing -->
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<version>${rest-assured.version}</version>
<scope>test</scope>
</dependency>
<!-- JSON Schema Validator -->
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>json-schema-validator</artifactId>
<version>${rest-assured.version}</version>
<scope>test</scope>
</dependency>
<!-- JUnit 5 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
3. Project Structure
project/
├── src/
│   ├── main/
│   │   └── java/
│   │       └── com/example/api/
│   └── test/
│       └── java/
│           └── com/example/api/
├── docs/
│   ├── apiary.apib          # API Blueprint documentation
│   └── dredd/
│       ├── dredd.yml        # Dredd configuration
│       └── hooks/           # JavaScript hooks
│           └── hooks.js
└── pom.xml

API Blueprint Documentation

1. Complete API Blueprint Example
# API Blueprint for User Management API
FORMAT: 1A
# User Management API
This API provides user management functionality including user creation, retrieval, updating, and deletion.
# Group Users
## User Collection [/api/v1/users]
### List Users [GET]
+ Response 200 (application/json)
+ Attributes (array[User])
### Create User [POST]
+ Request (application/json)
+ Attributes (CreateUserRequest)
+ Response 201 (application/json)
+ Headers
Location: /api/v1/users/550e8400-e29b-41d4-a716-446655440000
+ Attributes (User)
+ Response 400 (application/json)
+ Attributes (ErrorResponse)
## User [/api/v1/users/{id}]
+ Parameters
+ id (string, required) - User ID in UUID format
### Get User [GET]
+ Response 200 (application/json)
+ Attributes (User)
+ Response 404 (application/json)
+ Attributes (ErrorResponse)
### Update User [PUT]
+ Request (application/json)
+ Attributes (UpdateUserRequest)
+ Response 200 (application/json)
+ Attributes (User)
+ Response 404 (application/json)
+ Attributes (ErrorResponse)
+ Response 400 (application/json)
+ Attributes (ErrorResponse)
### Delete User [DELETE]
+ Response 204
+ Response 404 (application/json)
+ Attributes (ErrorResponse)
## User Activation [/api/v1/users/{id}/activate]
+ Parameters
+ id (string, required) - User ID in UUID format
### Activate User [POST]
+ Response 200 (application/json)
+ Attributes (User)
+ Response 404 (application/json)
+ Attributes (ErrorResponse)
# Data Structures
## User (object)
+ id: 550e8400-e29b-41d4-a716-446655440000 (string, required) - User ID
+ firstName: John (string, required) - First name
+ lastName: Doe (string, required) - Last name
+ email: [email protected] (string, required) - Email address
+ status: active (string, required) - User status
+ createdAt: 2023-01-01T00:00:00Z (string, required) - Creation timestamp
+ updatedAt: 2023-01-01T00:00:00Z (string, required) - Last update timestamp
## CreateUserRequest (object)
+ firstName: John (string, required) - First name
+ lastName: Doe (string, required) - Last name
+ email: [email protected] (string, required) - Email address
## UpdateUserRequest (object)
+ firstName: John (string, optional) - First name
+ lastName: Doe (string, optional) - Last name
+ status: active (string, optional) - User status
## ErrorResponse (object)
+ error: true (boolean, required) - Error indicator
+ message: User not found (string, required) - Error message
+ code: USER_NOT_FOUND (string, optional) - Error code
+ timestamp: 2023-01-01T00:00:00Z (string, required) - Error timestamp
2. Enhanced API Blueprint with Examples
# Enhanced User API with Examples
FORMAT: 1A
# User API
## Users Collection [/api/v1/users]
### List All Users [GET]
+ Response 200 (application/json)
+ Body
[
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"firstName": "John",
"lastName": "Doe",
"email": "[email protected]",
"status": "active",
"createdAt": "2023-01-01T00:00:00Z",
"updatedAt": "2023-01-01T00:00:00Z"
},
{
"id": "550e8400-e29b-41d4-a716-446655440001",
"firstName": "Jane",
"lastName": "Smith",
"email": "[email protected]",
"status": "inactive",
"createdAt": "2023-01-02T00:00:00Z",
"updatedAt": "2023-01-02T00:00:00Z"
}
]
### Create a New User [POST]
Create a new user in the system.
+ Request (application/json)
+ Attributes
+ firstName: John (string, required) - User's first name
+ lastName: Doe (string, required) - User's last name
+ email: [email protected] (string, required) - User's email address
+ Body
{
"firstName": "John",
"lastName": "Doe",
"email": "[email protected]"
}
+ Response 201 (application/json)
+ Headers
Location: /api/v1/users/550e8400-e29b-41d4-a716-446655440000
+ Body
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"firstName": "John",
"lastName": "Doe",
"email": "[email protected]",
"status": "active",
"createdAt": "2023-01-01T00:00:00Z",
"updatedAt": "2023-01-01T00:00:00Z"
}
+ Response 400 (application/json)
+ Body
{
"error": true,
"message": "Email already exists",
"code": "EMAIL_EXISTS",
"timestamp": "2023-01-01T00:00:00Z"
}
## User [/api/v1/users/{id}]
+ Parameters
+ id: 550e8400-e29b-41d4-a716-446655440000 (string, required) - The user ID
### Get User [GET]
Retrieve a specific user by ID.
+ Response 200 (application/json)
+ Body
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"firstName": "John",
"lastName": "Doe",
"email": "[email protected]",
"status": "active",
"createdAt": "2023-01-01T00:00:00Z",
"updatedAt": "2023-01-01T00:00:00Z"
}
+ Response 404 (application/json)
+ Body
{
"error": true,
"message": "User not found",
"code": "USER_NOT_FOUND",
"timestamp": "2023-01-01T00:00:00Z"
}

Dredd Configuration

1. Dredd Configuration File
# dredd.yml
dry-run: null
hookfiles: 
- "./docs/dredd/hooks/hooks.js"
language: nodejs
sandbox: false
server: null
server-wait: 3
init: false
custom: {}
names: false
only: []
output: 
- "cli"
header: []
sorted: false
user: null
inline-errors: false
details: false
method: []
color: true
level: info
timestamp: false
silent: false
path: []
hooks-worker-timeout: 5000
hooks-worker-connect-timeout: 1500
hooks-worker-connect-retry: 500
hooks-worker-after-connect-wait: 100
hooks-worker-term-timeout: 5000
hooks-worker-term-retry: 500
hooks-worker-handler-host: 127.0.0.1
hooks-worker-handler-port: 61321
config: ./dredd.yml
blueprint: ./docs/apiary.apib
endpoint: 'http://localhost:8080'
2. Package.json for Dredd Hooks
{
"name": "api-dredd-tests",
"version": "1.0.0",
"description": "Dredd tests for Java API",
"scripts": {
"test:dredd": "dredd",
"test:dredd:ci": "dredd --reporter=junit --output=reports/dredd.xml",
"test:dredd:html": "dredd --reporter=html --output=reports/dredd.html"
},
"devDependencies": {
"dredd": "latest",
"chai": "^4.3.7",
"sqlite3": "^5.1.6"
}
}
3. Dredd Hooks (JavaScript)
// docs/dredd/hooks/hooks.js
const hooks = require('dredd-hooks');
const chai = require('chai');
const expect = chai.expect;
// Test data storage
let testUsers = {};
let authToken = null;
hooks.beforeAll(function(transactions, done) {
console.log('Starting Dredd tests...');
// Initialize test data
testUsers = {
existingUser: {
id: '550e8400-e29b-41d4-a716-446655440000',
firstName: 'John',
lastName: 'Doe',
email: '[email protected]',
status: 'active',
createdAt: '2023-01-01T00:00:00Z',
updatedAt: '2023-01-01T00:00:00Z'
},
newUser: {
firstName: 'Jane',
lastName: 'Smith',
email: '[email protected]'
}
};
done();
});
hooks.beforeEach(function(transaction, done) {
// Add common headers
transaction.request.headers['Content-Type'] = 'application/json';
transaction.request.headers['Accept'] = 'application/json';
transaction.request.headers['User-Agent'] = 'Dredd/1.0.0';
// Add authentication if available
if (authToken) {
transaction.request.headers['Authorization'] = `Bearer ${authToken}`;
}
console.log(`Preparing test: ${transaction.name}`);
done();
});
// User-specific hooks
hooks.before('Users > User Collection > Create User', function(transaction, done) {
// Set up request body for user creation
transaction.request.body = JSON.stringify(testUsers.newUser);
done();
});
hooks.before('Users > User > Get User', function(transaction, done) {
// Replace {id} in URL with actual test user ID
const userId = testUsers.existingUser.id;
transaction.fullPath = transaction.fullPath.replace('{id}', userId);
transaction.request.uri = transaction.request.uri.replace('{id}', userId);
done();
});
hooks.before('Users > User > Update User', function(transaction, done) {
// Replace {id} and set update data
const userId = testUsers.existingUser.id;
transaction.fullPath = transaction.fullPath.replace('{id}', userId);
transaction.request.uri = transaction.request.uri.replace('{id}', userId);
const updateData = {
firstName: 'John Updated',
lastName: 'Doe Updated'
};
transaction.request.body = JSON.stringify(updateData);
done();
});
hooks.before('Users > User > Delete User', function(transaction, done) {
// Replace {id} in URL
const userId = testUsers.existingUser.id;
transaction.fullPath = transaction.fullPath.replace('{id}', userId);
transaction.request.uri = transaction.request.uri.replace('{id}', userId);
done();
});
// Response validation hooks
hooks.after('Users > User Collection > Create User', function(transaction, done) {
try {
const response = JSON.parse(transaction.real.body);
// Validate response structure
expect(response).to.have.property('id');
expect(response).to.have.property('firstName');
expect(response).to.have.property('lastName');
expect(response).to.have.property('email');
expect(response).to.have.property('status');
expect(response).to.have.property('createdAt');
expect(response).to.have.property('updatedAt');
// Store the created user for later tests
testUsers.createdUser = response;
console.log(`Created user with ID: ${response.id}`);
} catch (error) {
console.error('Response validation failed:', error.message);
}
done();
});
hooks.after('Users > User > Get User', function(transaction, done) {
if (transaction.real.statusCode === 200) {
try {
const response = JSON.parse(transaction.real.body);
// Validate user data
expect(response.id).to.equal(testUsers.existingUser.id);
expect(response.firstName).to.equal(testUsers.existingUser.firstName);
expect(response.email).to.equal(testUsers.existingUser.email);
} catch (error) {
console.error('Get user validation failed:', error.message);
}
}
done();
});
// Error scenario hooks
hooks.before('Users > User Collection > Create User > 400 Response', function(transaction, done) {
// Create a request that will cause a 400 error (duplicate email)
const duplicateUser = {
firstName: 'Duplicate',
lastName: 'User',
email: testUsers.existingUser.email // This should cause a conflict
};
transaction.request.body = JSON.stringify(duplicateUser);
done();
});
hooks.after('Users > User Collection > Create User > 400 Response', function(transaction, done) {
if (transaction.real.statusCode === 400) {
try {
const response = JSON.parse(transaction.real.body);
expect(response).to.have.property('error', true);
expect(response).to.have.property('message');
expect(response).to.have.property('timestamp');
} catch (error) {
console.error('Error response validation failed:', error.message);
}
}
done();
});
// Cleanup hooks
hooks.afterAll(function(transactions, done) {
console.log('Dredd tests completed');
console.log(`Total transactions: ${transactions.length}`);
// Log summary
const passed = transactions.filter(t => t.test && t.test.status === 'pass').length;
const failed = transactions.filter(t => t.test && t.test.status === 'fail').length;
const skipped = transactions.filter(t => t.test && t.test.status === 'skip').length;
console.log(`Results: ${passed} passed, ${failed} failed, ${skipped} skipped`);
done();
});
module.exports = hooks;

Java API Implementation

1. Spring Boot Application
package com.example.api;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class UserApiApplication {
public static void main(String[] args) {
SpringApplication.run(UserApiApplication.class, args);
}
}
2. Application Configuration
# application.yml
server:
port: 8080
servlet:
context-path: /
spring:
application:
name: user-api
jackson:
serialization:
write-dates-as-timestamps: false
date-format: com.fasterxml.jackson.databind.util.StdDateFormat
property-naming-strategy: SNAKE_CASE
management:
endpoints:
web:
exposure:
include: health,info
endpoint:
health:
show-details: always
logging:
level:
com.example.api: DEBUG
org.springframework.web: INFO
3. Domain Models
package com.example.api.model;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonInclude;
import jakarta.persistence.*;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import java.time.LocalDateTime;
import java.util.UUID;
@Entity
@Table(name = "users")
@JsonInclude(JsonInclude.Include.NON_NULL)
public class User {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
@NotBlank
@Size(max = 100)
@Column(nullable = false)
private String firstName;
@NotBlank
@Size(max = 100)
@Column(nullable = false)
private String lastName;
@NotBlank
@Email
@Size(max = 255)
@Column(nullable = false, unique = true)
private String email;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private UserStatus status = UserStatus.ACTIVE;
@Column(name = "created_at", nullable = false, updatable = false)
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss'Z'")
private LocalDateTime createdAt;
@Column(name = "updated_at", nullable = false)
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss'Z'")
private LocalDateTime updatedAt;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
}
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
// Constructors
public User() {}
public User(String firstName, String lastName, String email) {
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
}
// Getters and Setters
public UUID getId() { return id; }
public void setId(UUID id) { this.id = id; }
public String getFirstName() { return firstName; }
public void setFirstName(String firstName) { this.firstName = firstName; }
public String getLastName() { return lastName; }
public void setLastName(String lastName) { this.lastName = lastName; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public UserStatus getStatus() { return status; }
public void setStatus(UserStatus status) { this.status = status; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
}
enum UserStatus {
ACTIVE, INACTIVE, SUSPENDED
}
// DTOs for API
public class CreateUserRequest {
@NotBlank
@Size(max = 100)
private String firstName;
@NotBlank
@Size(max = 100)
private String lastName;
@NotBlank
@Email
@Size(max = 255)
private String email;
// Getters and Setters
public String getFirstName() { return firstName; }
public void setFirstName(String firstName) { this.firstName = firstName; }
public String getLastName() { return lastName; }
public void setLastName(String lastName) { this.lastName = lastName; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
}
public class UpdateUserRequest {
@Size(max = 100)
private String firstName;
@Size(max = 100)
private String lastName;
private UserStatus status;
// Getters and Setters
public String getFirstName() { return firstName; }
public void setFirstName(String firstName) { this.firstName = firstName; }
public String getLastName() { return lastName; }
public void setLastName(String lastName) { this.lastName = lastName; }
public UserStatus getStatus() { return status; }
public void setStatus(UserStatus status) { this.status = status; }
}
public class ErrorResponse {
private final boolean error = true;
private final String message;
private final String code;
private final LocalDateTime timestamp;
public ErrorResponse(String message) {
this(message, null);
}
public ErrorResponse(String message, String code) {
this.message = message;
this.code = code;
this.timestamp = LocalDateTime.now();
}
// Getters
public boolean isError() { return error; }
public String getMessage() { return message; }
public String getCode() { return code; }
public LocalDateTime getTimestamp() { return timestamp; }
}
4. Repository Layer
package com.example.api.repository;
import com.example.api.model.User;
import com.example.api.model.UserStatus;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
import java.util.UUID;
@Repository
public interface UserRepository extends JpaRepository<User, UUID> {
Optional<User> findByEmail(String email);
boolean existsByEmail(String email);
long countByStatus(UserStatus status);
}
5. Service Layer
package com.example.api.service;
import com.example.api.model.User;
import com.example.api.model.UserStatus;
import com.example.api.repository.UserRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
@Service
@Transactional
public class UserService {
private static final Logger LOG = LoggerFactory.getLogger(UserService.class);
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Transactional(readOnly = true)
public List<User> getAllUsers() {
LOG.debug("Fetching all users");
return userRepository.findAll();
}
@Transactional(readOnly = true)
public Optional<User> getUserById(UUID id) {
LOG.debug("Fetching user by ID: {}", id);
return userRepository.findById(id);
}
public User createUser(User user) {
LOG.info("Creating user with email: {}", user.getEmail());
if (userRepository.existsByEmail(user.getEmail())) {
throw new UserServiceException("User with email " + user.getEmail() + " already exists");
}
User savedUser = userRepository.save(user);
LOG.info("Successfully created user: {}", savedUser.getId());
return savedUser;
}
public User updateUser(UUID id, User userUpdate) {
LOG.info("Updating user: {}", id);
User existingUser = userRepository.findById(id)
.orElseThrow(() -> new UserServiceException("User not found: " + id));
// Update fields if provided
if (userUpdate.getFirstName() != null) {
existingUser.setFirstName(userUpdate.getFirstName());
}
if (userUpdate.getLastName() != null) {
existingUser.setLastName(userUpdate.getLastName());
}
if (userUpdate.getStatus() != null) {
existingUser.setStatus(userUpdate.getStatus());
}
User updatedUser = userRepository.save(existingUser);
LOG.info("Successfully updated user: {}", id);
return updatedUser;
}
public void deleteUser(UUID id) {
LOG.info("Deleting user: {}", id);
if (!userRepository.existsById(id)) {
throw new UserServiceException("User not found: " + id);
}
userRepository.deleteById(id);
LOG.info("Successfully deleted user: {}", id);
}
public User activateUser(UUID id) {
LOG.info("Activating user: {}", id);
User user = userRepository.findById(id)
.orElseThrow(() -> new UserServiceException("User not found: " + id));
user.setStatus(UserStatus.ACTIVE);
User activatedUser = userRepository.save(user);
LOG.info("Successfully activated user: {}", id);
return activatedUser;
}
}
class UserServiceException extends RuntimeException {
public UserServiceException(String message) {
super(message);
}
}
6. REST Controllers
package com.example.api.controller;
import com.example.api.model.*;
import com.example.api.service.UserService;
import jakarta.validation.Valid;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.net.URI;
import java.util.List;
import java.util.UUID;
@RestController
@RequestMapping("/api/v1/users")
public class UserController {
private static final Logger LOG = LoggerFactory.getLogger(UserController.class);
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping
public ResponseEntity<List<User>> getUsers() {
LOG.info("Fetching all users");
List<User> users = userService.getAllUsers();
return ResponseEntity.ok(users);
}
@GetMapping("/{id}")
public ResponseEntity<User> getUserById(@PathVariable UUID id) {
LOG.info("Fetching user by ID: {}", id);
return userService.getUserById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
public ResponseEntity<User> createUser(@Valid @RequestBody CreateUserRequest request) {
LOG.info("Creating user with email: {}", request.getEmail());
User user = new User(request.getFirstName(), request.getLastName(), request.getEmail());
User createdUser = userService.createUser(user);
return ResponseEntity
.created(URI.create("/api/v1/users/" + createdUser.getId()))
.body(createdUser);
}
@PutMapping("/{id}")
public ResponseEntity<User> updateUser(
@PathVariable UUID id,
@Valid @RequestBody UpdateUserRequest request) {
LOG.info("Updating user: {}", id);
User userUpdate = new User();
userUpdate.setFirstName(request.getFirstName());
userUpdate.setLastName(request.getLastName());
userUpdate.setStatus(request.getStatus());
try {
User updatedUser = userService.updateUser(id, userUpdate);
return ResponseEntity.ok(updatedUser);
} catch (UserServiceException e) {
return ResponseEntity.notFound().build();
}
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable UUID id) {
LOG.info("Deleting user: {}", id);
try {
userService.deleteUser(id);
return ResponseEntity.noContent().build();
} catch (UserServiceException e) {
return ResponseEntity.notFound().build();
}
}
@PostMapping("/{id}/activate")
public ResponseEntity<User> activateUser(@PathVariable UUID id) {
LOG.info("Activating user: {}", id);
try {
User activatedUser = userService.activateUser(id);
return ResponseEntity.ok(activatedUser);
} catch (UserServiceException e) {
return ResponseEntity.notFound().build();
}
}
@ExceptionHandler(UserServiceException.class)
public ResponseEntity<ErrorResponse> handleUserServiceException(UserServiceException e) {
LOG.warn("User service exception: {}", e.getMessage());
if (e.getMessage().contains("not found")) {
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponse(e.getMessage(), "USER_NOT_FOUND"));
} else if (e.getMessage().contains("already exists")) {
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(new ErrorResponse(e.getMessage(), "EMAIL_EXISTS"));
} else {
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(new ErrorResponse(e.getMessage()));
}
}
}

Testing Infrastructure

1. Test Configuration
# application-test.yml
spring:
datasource:
url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
driver-class-name: org.h2.Driver
username: sa
password: 
jpa:
database-platform: org.hibernate.dialect.H2Dialect
hibernate:
ddl-auto: create-drop
show-sql: false
h2:
console:
enabled: true
logging:
level:
com.example.api: DEBUG
2. Test Data Setup
package com.example.api.testdata;
import com.example.api.model.User;
import com.example.api.model.UserStatus;
import com.example.api.repository.UserRepository;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
import java.util.UUID;
@Component
@Profile("test")
public class TestDataLoader implements CommandLineRunner {
private final UserRepository userRepository;
public TestDataLoader(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public void run(String... args) throws Exception {
// Create test users for Dredd testing
User testUser = new User("John", "Doe", "[email protected]");
testUser.setId(UUID.fromString("550e8400-e29b-41d4-a716-446655440000"));
testUser.setStatus(UserStatus.ACTIVE);
userRepository.save(testUser);
System.out.println("Test data loaded successfully");
}
}
3. Integration Test Base Class
package com.example.api.integration;
import io.restassured.RestAssured;
import org.junit.jupiter.api.BeforeEach;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.test.context.ActiveProfiles;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("test")
public abstract class BaseIntegrationTest {
@LocalServerPort
protected int port;
@BeforeEach
protected void setUp() {
RestAssured.port = port;
RestAssured.basePath = "/api/v1";
RestAssured.enableLoggingOfRequestAndResponseIfValidationFails();
}
}
4. Dredd Test Runner
package com.example.api.dredd;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.context.ConfigurableApplicationContext;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.concurrent.TimeUnit;
public class DreddTestRunner {
private static final Logger LOG = LoggerFactory.getLogger(DreddTestRunner.class);
public static void main(String[] args) {
LOG.info("Starting Dredd API contract tests...");
// Start the Spring Boot application
ConfigurableApplicationContext context = null;
Process dreddProcess = null;
try {
// Start the application
context = SpringApplication.run(
com.example.api.UserApiApplication.class, 
"--spring.profiles.active=test"
);
// Wait for application to start
Thread.sleep(5000);
// Run Dredd tests
dreddProcess = runDreddTests();
int exitCode = dreddProcess.waitFor();
LOG.info("Dredd tests completed with exit code: {}", exitCode);
System.exit(exitCode);
} catch (Exception e) {
LOG.error("Dredd test execution failed", e);
System.exit(1);
} finally {
// Cleanup
if (context != null) {
context.close();
}
if (dreddProcess != null) {
dreddProcess.destroy();
}
}
}
private static Process runDreddTests() throws IOException, InterruptedException {
ProcessBuilder processBuilder = new ProcessBuilder();
if (System.getProperty("os.name").toLowerCase().contains("win")) {
processBuilder.command("cmd.exe", "/c", "npx", "dredd");
} else {
processBuilder.command("npx", "dredd");
}
processBuilder.directory(new java.io.File("."));
processBuilder.redirectErrorStream(true);
Process process = processBuilder.start();
// Read output
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
LOG.info("[Dredd] {}", line);
}
}
return process;
}
}

CI/CD Integration

1. Maven Configuration for Dredd
<!-- Add to pom.xml -->
<profiles>
<profile>
<id>dredd-tests</id>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.1.0</version>
<executions>
<execution>
<id>run-dredd-tests</id>
<phase>integration-test</phase>
<goals>
<goal>java</goal>
</goals>
<configuration>
<mainClass>com.example.api.dredd.DreddTestRunner</mainClass>
<classpathScope>test</classpathScope>
<arguments>
<argument>--spring.profiles.active=test</argument>
</arguments>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>com.github.eirslett</groupId>
<artifactId>frontend-maven-plugin</artifactId>
<version>1.12.1</version>
<executions>
<execution>
<id>install-node-and-npm</id>
<goals>
<goal>install-node-and-npm</goal>
</goals>
<configuration>
<nodeVersion>v18.16.0</nodeVersion>
<npmVersion>9.5.1</npmVersion>
</configuration>
</execution>
<execution>
<id>npm-install</id>
<goals>
<goal>npm</goal>
</goals>
<configuration>
<arguments>install</arguments>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
2. GitHub Actions Workflow
# .github/workflows/dredd.yml
name: API Contract Tests
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
dredd-tests:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
cache: 'maven'
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'
cache-dependency-path: package.json
- name: Install dependencies
run: |
npm install
mvn dependency:resolve
- name: Run Dredd tests
run: |
mvn clean test-compile -Pdredd-tests exec:java@run-dredd-tests
- name: Upload test results
uses: actions/upload-artifact@v3
if: always()
with:
name: dredd-reports
path: |
reports/
retention-days: 30
3. Docker Configuration for Testing
# Dockerfile for Dredd testing
FROM eclipse-temurin:21-jre
# Install Node.js and npm
RUN apt-get update && \
apt-get install -y curl && \
curl -fsSL https://deb.nodesource.com/setup_18.x | bash - && \
apt-get install -y nodejs
# Create app directory
WORKDIR /app
# Copy application JAR
COPY target/*.jar app.jar
# Copy API documentation and Dredd configuration
COPY docs/ ./docs/
COPY package*.json ./
# Install Dredd
RUN npm install
# Expose port
EXPOSE 8080
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8080/actuator/health || exit 1
# Run Dredd tests
CMD ["sh", "-c", "java -jar app.jar & sleep 10 && npx dredd"]

Advanced Dredd Hooks

1. Database Hooks
// docs/dredd/hooks/database-hooks.js
const hooks = require('dredd-hooks');
const sqlite3 = require('sqlite3').verbose();
let db = null;
hooks.beforeAll(function(transactions, done) {
// Initialize test database
db = new sqlite3.Database(':memory:');
db.serialize(function() {
db.run(`CREATE TABLE users (
id TEXT PRIMARY KEY,
first_name TEXT NOT NULL,
last_name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
status TEXT NOT NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
)`);
// Insert test data
const stmt = db.prepare(`INSERT INTO users 
(id, first_name, last_name, email, status, created_at, updated_at) 
VALUES (?, ?, ?, ?, ?, ?, ?)`);
stmt.run([
'550e8400-e29b-41d4-a716-446655440000',
'John',
'Doe',
'[email protected]',
'active',
'2023-01-01T00:00:00Z',
'2023-01-01T00:00:00Z'
]);
stmt.finalize();
});
done();
});
hooks.beforeEach(function(transaction, done) {
// Add database connection to transaction context
transaction.db = db;
done();
});
hooks.after('Users > User Collection > Create User', function(transaction, done) {
// Verify user was created in database
const response = JSON.parse(transaction.real.body);
db.get(
"SELECT * FROM users WHERE id = ?", 
[response.id], 
function(err, row) {
if (err) {
console.error('Database verification failed:', err);
} else if (row) {
console.log('User successfully created in database');
} else {
console.error('User not found in database after creation');
}
done();
}
);
});
hooks.afterAll(function(transactions, done) {
// Close database connection
if (db) {
db.close();
}
done();
});
2. Authentication Hooks
// docs/dredd/hooks/auth-hooks.js
const hooks = require('dredd-hooks');
const chai = require('chai');
const expect = chai.expect;
let authTokens = {};
hooks.before('Users > User Collection > Create User', function(transaction, done) {
// Add authentication for protected endpoints
if (!authTokens.admin) {
// Simulate login to get token
transaction.request.headers['Authorization'] = 'Bearer mock-admin-token';
authTokens.admin = 'mock-admin-token';
} else {
transaction.request.headers['Authorization'] = `Bearer ${authTokens.admin}`;
}
done();
});
hooks.before('Users > User > Get User', function(transaction, done) {
// Add authentication
transaction.request.headers['Authorization'] = 'Bearer mock-user-token';
done();
});
hooks.after('Users > User Collection > Create User', function(transaction, done) {
// Verify response includes proper headers
if (transaction.real.statusCode === 201) {
expect(transaction.real.headers).to.have.property('location');
expect(transaction.real.headers.location).to.match(/\/api\/v1\/users\/[a-f0-9-]+/);
}
done();
});
3. Performance Hooks
// docs/dredd/hooks/performance-hooks.js
const hooks = require('dredd-hooks');
const performanceThresholds = {
'Users > User Collection > List Users': 1000, // 1 second
'Users > User > Get User': 500, // 500ms
'Users > User Collection > Create User': 2000, // 2 seconds
};
hooks.beforeEach(function(transaction, done) {
// Start timing
transaction.startTime = Date.now();
done();
});
hooks.afterEach(function(transaction, done) {
// Check performance
const endTime = Date.now();
const duration = endTime - transaction.startTime;
const threshold = performanceThresholds[transaction.name];
if (threshold && duration > threshold) {
console.warn(`Performance warning: ${transaction.name} took ${duration}ms (threshold: ${threshold}ms)`);
}
done();
});

Best Practices

1. Dredd Configuration Best Practices
# dredd-best-practices.yml
# Comprehensive Dredd configuration
dry-run: null
hookfiles: 
- "./docs/dredd/hooks/*.js"
language: nodejs
sandbox: false
server: "java -jar target/api.jar --spring.profiles.active=test"
server-wait: 10
init: false
custom: 
api-name: "User Management API"
environment: "test"
names: false
only: []
output: 
- "cli"
- "junit"
- "html"
header: 
- "User-Agent: Dredd/1.0.0"
- "Accept: application/json"
sorted: false
user: null
inline-errors: false
details: false
method: []
color: true
level: verbose
timestamp: false
silent: false
path: []
hooks-worker-timeout: 10000
hooks-worker-connect-timeout: 5000
hooks-worker-connect-retry: 1000
hooks-worker-after-connect-wait: 2000
hooks-worker-term-timeout: 10000
hooks-worker-term-retry: 1000
config: ./dredd.yml
blueprint: ./docs/apiary.apib
endpoint: 'http://localhost:8080'
reporter: 
- "junit"
- "html"
output: 
- "reports/dredd.junit.xml"
- "reports/dredd.html"
2. API Blueprint Best Practices
# API Blueprint Best Practices Template
FORMAT: 1A
# API Name
Overview of the API and its purpose.
# Authentication
Describe authentication mechanism if any.
## Access Token [POST /auth/token]
+ Request (application/json)
+ Attributes
+ username: [email protected] (string, required)
+ password: secret (string, required)
+ Response 200 (application/json)
+ Attributes
+ access_token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9 (string, required)
+ token_type: bearer (string, required)
+ expires_in: 3600 (number, required)
# Group Resource Group
Description of the resource group.
## Resource Collection [/resources]
### Operation Name [VERB]
Description of the operation.
+ Parameters
+ param_name (type, optional) - Parameter description
+ Request (content-type)
+ Headers
Header-Name: header-value
+ Attributes (DataStructureName)
+ Body
Example request body
+ Response Status (content-type)
+ Headers
Header-Name: header-value
+ Attributes (DataStructureName)
+ Body
Example response body
# Data Structures
## DataStructureName (object)
+ property: value (type, required) - Property description
+ optionalProperty: value (type, optional) - Optional property description

Conclusion

Implementing Dredd for API Blueprint testing in Java provides:

  • Contract Validation: Ensure API implementation matches documentation
  • Automated Testing: Continuous validation of API contracts
  • Documentation Accuracy: Keep API Blueprint in sync with implementation
  • Regression Prevention: Catch breaking changes early

Key benefits demonstrated:

  1. Language-agnostic testing with Dredd
  2. Comprehensive hook system for test setup and validation
  3. CI/CD integration for automated contract testing
  4. Performance monitoring alongside functional testing
  5. Database integration for end-to-end validation
  6. Multiple report formats for different needs

This approach ensures your Java API remains consistent with its documentation, providing reliable API contracts for consumers while maintaining development velocity through automated testing.

Leave a Reply

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


Macro Nepal Helper