Testing
Learn how to test applications that use the .NET Custody SDK with built-in mock clients, test data builders, and testing best practices.
Mock Mode
The SDK includes a built-in mock mode that returns realistic test data without making actual API calls. Perfect for unit tests, integration tests, and development.
Enable Mock Mode in Configuration
using CoxAuto.Vecu.CustodySdk.DependencyInjection;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddCustodyClient(options =>
{
options.Environment = CustodyEnvironment.Sandbox;
options.UseMockData = true; // Enable mock mode
});
When mock mode is enabled:
- ✅ No authentication required
- ✅ No network calls made
- ✅ Deterministic, predictable responses
- ✅ In-memory authorization storage
- ✅ Fast test execution
MockCustodyServiceClient
Use the built-in MockCustodyServiceClient for complete control over mock behavior:
using CoxAuto.Vecu.CustodySdk.Mock;
using CoxAuto.Vecu.CustodySdk.Models.Requests;
// Basic usage with default success behavior
var mockClient = new MockCustodyServiceClient();
var request = new CreateAuthorizationRequest
{
Vin = "1TEST5678MOCK1234",
Origin = "500 Commerce Rd, Richmond, VA 23234",
Destination = "750 Industrial Pkwy, Dallas, TX 75247",
PersonIdentityKey = "DL-109-283-745",
MakeModel = "Toyota Camry",
AuthorizedBy = "TEST_SYSTEM"
};
var response = await mockClient.CreateAuthorizationAsync(request);
Console.WriteLine($"Authorization created: {response.AuthorizationId}");
Console.WriteLine($"Status: {response.Status}"); // PENDING
Configuring Mock Behavior
Use MockResponseConfiguration to customize mock responses:
using CoxAuto.Vecu.CustodySdk.Mock;
// Configure custom behaviors
var config = new MockResponseConfiguration
{
DefaultBehavior = MockBehavior.Success,
ThrowOnError = true,
SimulatedDelay = TimeSpan.FromMilliseconds(50) // Simulate network latency
};
// Configure specific authorization behaviors
config.AuthorizationBehaviors["auth-404"] = MockBehavior.NotFound;
config.AuthorizationBehaviors["auth-expired"] = MockBehavior.Expired;
config.AuthorizationBehaviors["auth-duplicate"] = MockBehavior.Duplicate;
var mockClient = new MockCustodyServiceClient(config);
// This will throw AuthorizationNotFoundException
try
{
var auth = await mockClient.GetAuthorizationAsync("auth-404");
}
catch (AuthorizationNotFoundException ex)
{
Console.WriteLine($"Expected: {ex.Message}");
}
MockBehavior Options
Configure different error scenarios for testing:
SuccessMockBehaviorReturn successful response with valid mock data
NotFoundMockBehaviorThrow AuthorizationNotFoundException (404)
ValidationErrorMockBehaviorThrow ValidationException (400, 422)
DuplicateMockBehaviorThrow DuplicateAuthorizationException (409)
NotReleasableMockBehaviorThrow VehicleNotReleasableException (409)
RateLimitMockBehaviorThrow RateLimitException (429)
NetworkErrorMockBehaviorThrow NetworkException (5xx)
ExpiredMockBehaviorReturn authorization with Expired status
AlreadyCancelledMockBehaviorThrow ValidationException for already cancelled authorization
MockResponseConfiguration Fluent API
Chain configuration methods for cleaner setup:
var config = new MockResponseConfiguration()
.WithBehavior("auth-404", MockBehavior.NotFound)
.WithBehavior("auth-expired", MockBehavior.Expired)
.WithBehavior("auth-duplicate", MockBehavior.Duplicate)
.WithDelay(TimeSpan.FromMilliseconds(100));
var mockClient = new MockCustodyServiceClient(config);
TestDataBuilder
Use TestDataBuilder for fluent test data creation with sensible defaults:
using CoxAuto.Vecu.CustodySdk.Testing;
using CoxAuto.Vecu.CustodySdk.Models.Enums;
// Create authorization request with defaults
var request = TestDataBuilder
.CreateAuthorizationRequest()
.WithVin("1TEST5678MOCK1234")
.WithOrigin("500 Commerce Rd, Richmond, VA 23234")
.WithDestination("750 Industrial Pkwy, Dallas, TX 75247")
.WithValidUntil(DateTimeOffset.UtcNow.AddHours(48))
.Build();
// Create authorization response
var response = TestDataBuilder
.AuthorizationResponse()
.WithAuthorizationId("AUTH-TEST-123")
.WithStatus(AuthorizationStatus.RELEASED)
.WithExpiresAt(DateTimeOffset.UtcNow.AddHours(24))
.Build();
// Create authorization details
var details = TestDataBuilder
.AuthorizationDetails()
.WithAuthorizationId("AUTH-TEST-123")
.WithStatus(AuthorizationStatus.PENDING)
.WithRole(AuthorizationRole.DRIVER)
.Build();
All builders provide sensible defaults, so you only need to customize what matters for your test:
// Minimal test data - uses all defaults
var request = TestDataBuilder
.CreateAuthorizationRequest()
.Build();
Console.WriteLine(request.Vin); // Auto-generated valid VIN
Console.WriteLine(request.Origin); // "500 Commerce Rd, Richmond, VA 23234"
Console.WriteLine(request.Destination); // "750 Industrial Pkwy, Dallas, TX 75247"
xUnit Testing Examples
Basic Unit Test
using Xunit;
using CoxAuto.Vecu.CustodySdk.Mock;
using CoxAuto.Vecu.CustodySdk.Testing;
using CoxAuto.Vecu.CustodySdk.Models.Enums;
public class AuthorizationServiceTests
{
[Fact]
public async Task CreateAuthorization_ValidRequest_ReturnsAuthorization()
{
// Arrange
var mockClient = new MockCustodyServiceClient();
var service = new AuthorizationService(mockClient);
var request = TestDataBuilder
.CreateAuthorizationRequest()
.WithVin("1TEST5678MOCK1234")
.Build();
// Act
var result = await service.CreateAuthorizationAsync(request);
// Assert
Assert.NotNull(result);
Assert.NotNull(result.AuthorizationId);
Assert.Equal(AuthorizationStatus.PENDING, result.Status);
Assert.Equal("1TEST5678MOCK1234", result.Vin);
}
[Fact]
public async Task CreateAuthorization_DuplicateVin_ThrowsDuplicateException()
{
// Arrange
var config = new MockResponseConfiguration()
.WithBehavior("auth-duplicate", MockBehavior.Duplicate);
config.DefaultBehavior = MockBehavior.Duplicate;
var mockClient = new MockCustodyServiceClient(config);
var service = new AuthorizationService(mockClient);
var request = TestDataBuilder
.CreateAuthorizationRequest()
.Build();
// Act & Assert
var ex = await Assert.ThrowsAsync<DuplicateAuthorizationException>(
() => service.CreateAuthorizationAsync(request)
);
Assert.NotNull(ex.ExistingAuthorizationId);
}
[Fact]
public async Task GetAuthorization_NotFound_ThrowsNotFoundException()
{
// Arrange
var config = new MockResponseConfiguration()
.WithBehavior("auth-404", MockBehavior.NotFound);
var mockClient = new MockCustodyServiceClient(config);
// Act & Assert
await Assert.ThrowsAsync<AuthorizationNotFoundException>(
() => mockClient.GetAuthorizationAsync("auth-404")
);
}
[Fact]
public async Task WaitForAuthorization_CompletesSuccessfully_ReturnsCompletedStatus()
{
// Arrange
var mockClient = new MockCustodyServiceClient();
var request = TestDataBuilder
.CreateAuthorizationRequest()
.Build();
var createResponse = await mockClient.CreateAuthorizationAsync(request);
// Act
var result = await mockClient.WaitForAuthorizationAsync(
createResponse.AuthorizationId,
new WaitOptions
{
TimeoutSeconds = 10,
PollingIntervalSeconds = 1
}
);
// Assert
Assert.Equal(AuthorizationStatus.RELEASED, result.Status);
Assert.NotNull(result.ExpiresAt);
}
}
Testing with Dependency Injection
using Microsoft.Extensions.DependencyInjection;
using Xunit;
public class IntegrationTests : IDisposable
{
private readonly ServiceProvider _serviceProvider;
private readonly ICustodyServiceClient _client;
public IntegrationTests()
{
var services = new ServiceCollection();
// Register mock client
services.AddCustodyClient(options =>
{
options.Environment = CustodyEnvironment.Sandbox;
options.UseMockData = true;
});
services.AddSingleton<AuthorizationService>();
_serviceProvider = services.BuildServiceProvider();
_client = _serviceProvider.GetRequiredService<ICustodyServiceClient>();
}
[Fact]
public async Task ServiceIntegration_CreateAndRetrieve_Success()
{
// Arrange
var service = _serviceProvider.GetRequiredService<AuthorizationService>();
// Act
var authId = await service.CreateAndGetIdAsync(
"1TEST5678MOCK1234",
"500 Commerce Rd, Richmond, VA 23234",
"750 Industrial Pkwy, Dallas, TX 75247"
);
var details = await service.GetDetailsAsync(authId);
// Assert
Assert.NotNull(details);
Assert.Equal(authId, details.AuthorizationId);
}
public void Dispose()
{
_serviceProvider.Dispose();
}
}
Testing Error Scenarios
public class ErrorHandlingTests
{
[Fact]
public async Task CreateAuthorization_ValidationError_HandlesGracefully()
{
// Arrange
var config = new MockResponseConfiguration
{
DefaultBehavior = MockBehavior.ValidationError
};
var mockClient = new MockCustodyServiceClient(config);
var request = TestDataBuilder
.CreateAuthorizationRequest()
.Build();
// Act & Assert
var ex = await Assert.ThrowsAsync<ValidationException>(
() => mockClient.CreateAuthorizationAsync(request)
);
Assert.NotNull(ex.ValidationErrors);
Assert.Contains(ex.ValidationErrors, kvp => kvp.Value.Length > 0);
}
[Fact]
public async Task GetAuthorization_RateLimited_ThrowsRateLimitException()
{
// Arrange
var config = new MockResponseConfiguration()
.WithBehavior("auth-ratelimit", MockBehavior.RateLimit);
var mockClient = new MockCustodyServiceClient(config);
// Act & Assert
var ex = await Assert.ThrowsAsync<RateLimitException>(
() => mockClient.GetAuthorizationAsync("auth-ratelimit")
);
Assert.NotNull(ex.RetryAfter);
Assert.True(ex.RetryAfter.Value.TotalSeconds > 0);
}
[Theory]
[InlineData("auth-404", MockBehavior.NotFound)]
[InlineData("auth-expired", MockBehavior.Expired)]
[InlineData("auth-network-error", MockBehavior.NetworkError)]
public async Task GetAuthorization_VariousErrors_ThrowsExpectedException(
string authId,
MockBehavior behavior)
{
// Arrange
var config = new MockResponseConfiguration()
.WithBehavior(authId, behavior);
var mockClient = new MockCustodyServiceClient(config);
// Act & Assert
var exceptionThrown = false;
try
{
await mockClient.GetAuthorizationAsync(authId);
}
catch (CustodyException)
{
exceptionThrown = true;
}
Assert.True(exceptionThrown);
}
}
Mocking with Moq
For more advanced mocking scenarios, use Moq with the ICustodyServiceClient interface:
using Moq;
using Xunit;
using CoxAuto.Vecu.CustodySdk.Client;
using CoxAuto.Vecu.CustodySdk.Testing;
public class ServiceWithMoqTests
{
[Fact]
public async Task ProcessAuthorization_Success_CallsGetAndCancel()
{
// Arrange
var mockClient = new Mock<ICustodyServiceClient>();
var authDetails = TestDataBuilder
.AuthorizationDetails()
.WithAuthorizationId("AUTH-TEST-123")
.WithStatus(AuthorizationStatus.PENDING)
.Build();
var cancelResponse = new CancelAuthorizationResponse
{
AuthorizationId = "AUTH-TEST-123",
Vin = "1TEST5678MOCK1234",
Status = AuthorizationStatus.CANCELLED,
CancelledAt = DateTimeOffset.UtcNow,
CancelledBy = "TEST_SYSTEM",
CancellationReason = "Cancelled for testing - driver reassigned"
};
mockClient
.Setup(c => c.GetAuthorizationAsync(
It.IsAny<string>(),
It.IsAny<CancellationToken>()
))
.ReturnsAsync(authDetails);
mockClient
.Setup(c => c.CancelAuthorizationAsync(
It.IsAny<string>(),
It.IsAny<CancelAuthorizationRequest>(),
It.IsAny<CancellationToken>()
))
.ReturnsAsync(cancelResponse);
var service = new AuthorizationService(mockClient.Object);
// Act
await service.ProcessAndCancelAsync("AUTH-TEST-123");
// Assert
mockClient.Verify(
c => c.GetAuthorizationAsync("AUTH-TEST-123", It.IsAny<CancellationToken>()),
Times.Once
);
mockClient.Verify(
c => c.CancelAuthorizationAsync(
"AUTH-TEST-123",
It.IsAny<CancelAuthorizationRequest>(),
It.IsAny<CancellationToken>()
),
Times.Once
);
}
[Fact]
public async Task GetAuthorization_ThrowsException_HandlesGracefully()
{
// Arrange
var mockClient = new Mock<ICustodyServiceClient>();
mockClient
.Setup(c => c.GetAuthorizationAsync(
It.IsAny<string>(),
It.IsAny<CancellationToken>()
))
.ThrowsAsync(new AuthorizationNotFoundException("AUTH-404"));
var service = new AuthorizationService(mockClient.Object);
// Act & Assert
await Assert.ThrowsAsync<AuthorizationNotFoundException>(
() => service.GetDetailsAsync("AUTH-404")
);
}
}
Integration Testing with Sandbox
Test against the real Sandbox environment for end-to-end validation:
using Xunit;
using Microsoft.Extensions.Configuration;
public class SandboxIntegrationTests : IDisposable
{
private readonly ICustodyServiceClient _client;
private readonly IConfiguration _configuration;
public SandboxIntegrationTests()
{
// Load configuration from appsettings.Test.json
_configuration = new ConfigurationBuilder()
.AddJsonFile("appsettings.Test.json")
.AddEnvironmentVariables()
.Build();
var services = new ServiceCollection();
services.AddCustodyClient(options =>
{
options.Environment = CustodyEnvironment.Sandbox;
options.TokenProvider = async (ct) =>
{
// Use your auth system to get a bearer token
return _configuration["CustodyClient:TestToken"]
?? throw new InvalidOperationException("Test token not configured");
};
options.TimeoutSeconds = 60;
});
var serviceProvider = services.BuildServiceProvider();
_client = serviceProvider.GetRequiredService<ICustodyServiceClient>();
}
[Fact(Skip = "Integration test - run manually")]
public async Task CreateAuthorization_RealSandbox_Success()
{
// Arrange
var request = TestDataBuilder
.CreateAuthorizationRequest()
.WithVin("1TEST5678SANDBOX1")
.WithOrigin("100 Sandbox Dr, Phoenix, AZ 85001")
.WithDestination("250 Test Blvd, Denver, CO 80202")
.Build();
// Act
var response = await _client.CreateAuthorizationAsync(request);
// Assert
Assert.NotNull(response);
Assert.NotEmpty(response.AuthorizationId);
Assert.Equal(AuthorizationStatus.PENDING, response.Status);
// Cleanup
await _client.CancelAuthorizationAsync(
response.AuthorizationId,
new CancelAuthorizationRequest
{
CancellationReason = "Test cleanup - sandbox integration test complete"
}
);
}
public void Dispose()
{
// Cleanup resources
}
}
appsettings.Test.json
{
"CustodyClient": {
"TestToken": "your-sandbox-test-token-here"
}
}
Mark integration tests with [Fact(Skip = "...")] or a custom trait to
prevent running them in CI/CD pipelines unless explicitly enabled.
Best Practices
1. Use Mock Mode for Unit Tests
Mock mode is faster and doesn't require network access:
// ✅ GOOD: Fast, deterministic, no network
builder.Services.AddCustodyClient(options =>
{
options.UseMockData = true;
});
// ❌ AVOID: Slow, requires token provider, network dependency
builder.Services.AddCustodyClient(options =>
{
options.Environment = CustodyEnvironment.Sandbox;
options.TokenProvider = async (ct) => await GetRealTokenAsync(ct);
});
2. Test Error Scenarios
Always test how your code handles exceptions:
[Fact]
public async Task Service_HandlesNotFound_ReturnsNull()
{
var config = new MockResponseConfiguration()
.WithBehavior("not-found", MockBehavior.NotFound);
var mockClient = new MockCustodyServiceClient(config);
var service = new AuthorizationService(mockClient);
var result = await service.TryGetAuthorizationAsync("not-found");
Assert.Null(result);
}
3. Use TestDataBuilder for Consistency
Create test data with sensible defaults:
// ✅ GOOD: Readable, maintainable, consistent
var request = TestDataBuilder
.CreateAuthorizationRequest()
.WithVin("1TEST5678MOCK1234")
.Build();
// ❌ AVOID: Verbose, error-prone, inconsistent
var request = new CreateAuthorizationRequest
{
Vin = "1TEST5678MOCK1234",
Origin = "500 Commerce Rd, Richmond, VA 23234",
Destination = "750 Industrial Pkwy, Dallas, TX 75247",
PersonIdentityKey = "DL-109-283-745",
MakeModel = "Toyota Camry",
AuthorizedBy = "SYSTEM"
};
4. Test Async Patterns Properly
Use async/await correctly in tests:
// ✅ GOOD: Proper async test
[Fact]
public async Task CreateAuthorization_Success()
{
var result = await mockClient.CreateAuthorizationAsync(request);
Assert.NotNull(result);
}
// ❌ WRONG: Missing async/await
[Fact]
public void CreateAuthorization_Success()
{
var result = mockClient.CreateAuthorizationAsync(request).Result; // Blocks!
Assert.NotNull(result);
}
5. Dispose Resources Properly
Use IDisposable pattern for cleanup:
public class MyTests : IDisposable
{
private readonly MockCustodyServiceClient _mockClient;
public MyTests()
{
_mockClient = new MockCustodyServiceClient();
}
[Fact]
public async Task MyTest()
{
// Test using _mockClient
}
public void Dispose()
{
_mockClient.Dispose();
}
}
Next Steps
- Configuration - Configure mock mode in applications
- Error Handling - Test exception scenarios
- API Reference - Complete SDK documentation