This document explains the contract testing system implemented as part of Phase 2 of the API Stability Implementation Plan.
Contract testing verifies that implementations of interfaces adhere to their behavioral expectations (the "contract") beyond just satisfying the method signatures. While type checking ensures that implementations provide the required methods with the correct signatures, contract testing ensures they behave correctly.
- API Stability: Ensures consistent behavior across releases
- Documentation: Serves as executable documentation for interface behavioral requirements
- Alternative Implementations: Helps users create compatible alternative implementations
- Breaking Change Detection: Identifies behavioral changes that might not be caught by type checking
- Interface Evolution: Provides a framework for evolving interfaces while maintaining compatibility
Each core interface in the SDK has a corresponding contract test that verifies:
- Method Behavior: Does each method behave according to its contract?
- Error Handling: Are errors returned in the expected situations?
- Thread Safety: Does the implementation handle concurrent access correctly?
- Resource Management: Are resources properly managed and released?
- Context Handling: Does the implementation respect context cancellation?
The ClientInterface contract test verifies that any implementation:
- Returns a non-nil HTTP client from
GetHTTPClient() - Returns a non-empty string from
GetBaseURL() - Returns a non-empty string from
GetUserAgent() - Returns a non-nil logger from
GetLogger() - Respects context cancellation in the
Domethod - Returns an error when given a nil request
- Returns consistent values for configuration getters
The SDK's own implementations are tested against these contracts to ensure they behave correctly:
func TestClientImplementationContract(t *testing.T) {
client := core.NewClient(/* options... */)
contracts.VerifyClientContract(t, client)
}Users who create alternative implementations can use these contract tests to verify compatibility:
func TestMyCustomClientContract(t *testing.T) {
client := NewMyCustomClient()
contracts.VerifyClientContract(t, client)
}Users can create their own contract tests for custom interfaces following the same pattern:
// Define contract test for custom interface
func VerifyMyServiceContract(t *testing.T, service MyServiceInterface) {
t.Helper()
t.Run("MethodA", func(t *testing.T) {
// Test method behavior
})
// ... test other methods
}
// Use it in tests
func TestMyServiceImplementation(t *testing.T) {
service := NewMyService()
VerifyMyServiceContract(t, service)
}The contracts package includes mock implementations of all core interfaces for use in testing:
MockClient: A simpleClientInterfaceimplementationMockTransport: A simpleTransportimplementationMockAuthorizer: A simpleAuthorizerimplementationMockLogger: A simpleLoggerimplementation- etc.
These mocks implement the interface contracts and can be used in your own tests:
func TestMyCode(t *testing.T) {
client := contracts.NewMockClient()
// Use the mock client in your tests
}Contract tests are provided for the following interfaces:
| Interface | Description | Contract Test |
|---|---|---|
ClientInterface |
Core client functionality | VerifyClientContract |
Transport |
HTTP transport functionality | VerifyTransportContract |
ConnectionPool |
Connection pool functionality | VerifyConnectionPoolContract |
ConnectionPoolManager |
Connection pool management | VerifyConnectionPoolManagerContract |
Authorizer |
Authorization functionality | VerifyAuthorizerContract |
TokenManager |
Token management and refresh | VerifyTokenManagerContract |
Logger |
Logging functionality | VerifyLoggerContract |
- Focus on Behavior: Test the behavior described in the documentation, not implementation details
- Test Edge Cases: Include tests for error conditions, resource exhaustion, etc.
- Use Clear Test Names: Make it obvious what behavior is being tested
- Minimize Dependencies: Avoid dependencies on external services
- Document Requirements: Clearly document the behavioral requirements being tested
- Read the Contract Tests: Understand the behavioral expectations
- Test Early and Often: Run contract tests during development
- Focus on Compatibility: Ensure compatibility with existing clients
- Document Deviations: If you must deviate from the contract, document it clearly
- Extend, Don't Modify: Add new methods rather than changing existing ones
Contract testing is part of Phase 2 of the API Stability Implementation Plan and works together with:
- API Stability Indicators: Package stability levels (STABLE, BETA, etc.)
- API Compatibility Tools: Tools to detect API changes between versions
- Deprecation System: System for marking and tracking deprecated features
- CI Integration: Automated verification of API compatibility
By enforcing behavioral contracts, we ensure that the SDK's API remains stable and predictable across releases, while still allowing for evolution and improvement.
Planned enhancements to the contract testing system include:
- Automated Contract Generation: Generate contract tests from interface documentation
- Performance Contracts: Verify performance characteristics
- Contract Visualization: Visualize interface contracts and relationships
- Test Coverage Analysis: Analyze coverage of interface contracts
- Contract Versioning: Support for versioned contracts to track evolution
Contract testing provides a foundation for API stability by ensuring that implementations adhere to their behavioral contracts. By documenting and testing these contracts, we can maintain compatibility while evolving the SDK to meet new requirements.
The contract testing system is available in the pkg/core/contracts package and is designed to be used by both SDK developers and users implementing custom interfaces.