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