SDK Testing Guide
This guide shows you how to write effective tests for applications integrating the AnkaSecure SDK, including unit tests with mocking and integration tests against the platform.
Testing Strategy
Test Pyramid
Unit Tests (80%): - Test business logic in isolation - Mock AnkaSecure SDK calls - Fast execution (<1 second per test)
Integration Tests (15%): - Test against real AnkaSecure API - Use test tenant (not production) - Slower execution (seconds per test)
End-to-End Tests (5%): - Test complete user workflows - Production-like environment - Slowest execution (minutes per test)
Unit Testing with Mocking
Mock the AnkaSecureClient
Use Mockito to mock SDK client:
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;
import co.ankatech.ankasecure.sdk.AnkaSecureClient;
import co.ankatech.ankasecure.sdk.model.*;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.BeforeEach;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
public class CustomerServiceTest {
@Mock
private AnkaSecureClient ankaSecureClient;
private CustomerService customerService;
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
customerService = new CustomerService(ankaSecureClient);
}
@Test
void testEncryptCustomerData_Success() {
// Given: Mock successful encryption
EncryptRequest request = EncryptRequest.builder()
.keyId("customer-data-key")
.plaintext("sensitive-data")
.build();
EncryptResponse mockResponse = EncryptResponse.builder()
.ciphertext("eyJhbGciOiJNTC1LRU0tNzY4In0...")
.keyUsed("customer-data-key")
.algorithm("ML-KEM-768")
.build();
when(ankaSecureClient.encrypt(any(EncryptRequest.class)))
.thenReturn(mockResponse);
// When: Encrypt customer data
String result = customerService.encryptCustomerData("sensitive-data");
// Then: Verify encryption called and result returned
assertNotNull(result);
assertEquals("eyJhbGciOiJNTC1LRU0tNzY4In0...", result);
verify(ankaSecureClient, times(1)).encrypt(any(EncryptRequest.class));
}
@Test
void testEncryptCustomerData_Failure() {
// Given: Mock encryption failure
when(ankaSecureClient.encrypt(any(EncryptRequest.class)))
.thenThrow(new AnkaSecureException("KEY_001", "Key not found"));
// When/Then: Expect exception
assertThrows(DataEncryptionException.class, () -> {
customerService.encryptCustomerData("sensitive-data");
});
}
}
Mock Responses
Create reusable mock responses:
public class AnkaSecureMocks {
public static EncryptResponse mockEncryptResponse(String keyId, String algorithm) {
return EncryptResponse.builder()
.ciphertext("mock-ciphertext-" + UUID.randomUUID())
.keyUsed(keyId)
.algorithm(algorithm)
.build();
}
public static DecryptResponse mockDecryptResponse(String plaintext) {
return DecryptResponse.builder()
.plaintext(plaintext)
.keyUsed("mock-key")
.algorithm("ML-KEM-768")
.build();
}
public static KeyResponse mockKeyResponse(String keyId, String algorithm) {
return KeyResponse.builder()
.keyId(keyId)
.algorithm(algorithm)
.status("ACTIVE")
.createdAt(Instant.now())
.build();
}
}
Usage:
when(ankaSecureClient.encrypt(any()))
.thenReturn(AnkaSecureMocks.mockEncryptResponse("key-123", "ML-KEM-768"));
Integration Testing
Test Against Real API
Use Test Tenant (not production):
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class AnkaSecureIntegrationTest {
@Value("${ankasecure.api.key}")
private String apiKey;
@Value("${ankasecure.tenant.id}")
private String tenantId;
private AnkaSecureClient client;
@BeforeEach
void setUp() {
ClientConfig config = ClientConfig.builder()
.baseUrl("https://api.ankasecure.com")
.apiKey(apiKey)
.tenant(tenantId)
.build();
client = new AnkaSecureClient(config);
}
@Test
void testEndToEndEncryptDecrypt() {
// Generate test key
KeyGenerationRequest keyRequest = KeyGenerationRequest.builder()
.algorithm("ML-KEM-768")
.keyId("integration-test-key-" + UUID.randomUUID())
.build();
KeyResponse key = client.generateKey(keyRequest);
assertEquals("ACTIVE", key.getStatus());
// Encrypt
EncryptRequest encryptRequest = EncryptRequest.builder()
.keyId(key.getKeyId())
.plaintext("Integration test data")
.build();
EncryptResponse encrypted = client.encrypt(encryptRequest);
assertNotNull(encrypted.getCiphertext());
// Decrypt
DecryptRequest decryptRequest = DecryptRequest.builder()
.ciphertext(encrypted.getCiphertext())
.build();
DecryptResponse decrypted = client.decrypt(decryptRequest);
assertEquals("Integration test data", decrypted.getPlaintext());
// Cleanup: Delete test key
client.deleteKey(key.getKeyId());
}
@Test
void testKeyRotation() {
// Generate old key
KeyResponse oldKey = client.generateKey(
KeyGenerationRequest.builder()
.algorithm("RSA-2048")
.keyId("old-rsa-key-" + UUID.randomUUID())
.build()
);
// Encrypt with old key
EncryptResponse encrypted = client.encrypt(
EncryptRequest.builder()
.keyId(oldKey.getKeyId())
.plaintext("Data to rotate")
.build()
);
// Generate new PQC key
KeyResponse newKey = client.generateKey(
KeyGenerationRequest.builder()
.algorithm("ML-KEM-768")
.keyId("new-mlkem-key-" + UUID.randomUUID())
.build()
);
// Re-encrypt (rotation)
ReencryptResponse reencrypted = client.reencrypt(
ReencryptRequest.builder()
.ciphertext(encrypted.getCiphertext())
.newKeyId(newKey.getKeyId())
.build()
);
// Verify decryption with new key
DecryptResponse decrypted = client.decrypt(
DecryptRequest.builder()
.ciphertext(reencrypted.getCiphertext())
.build()
);
assertEquals("Data to rotate", decrypted.getPlaintext());
// Cleanup
client.deleteKey(oldKey.getKeyId());
client.deleteKey(newKey.getKeyId());
}
}
Test Configuration
application-test.yml:
ankasecure:
api:
base-url: https://api.ankasecure.com
key: ${ANKASECURE_API_KEY_TEST} # Test tenant API key
tenant:
id: ${ANKASECURE_TENANT_ID_TEST} # Test tenant ID
connection:
timeout: 30000
max-retries: 3
Environment Variables (.env.test):
Add to .gitignore:
Test Data Management
Generate Test Keys
Best Practice: Generate unique keys per test (avoid key ID collisions)
public class TestDataFactory {
public static String generateTestKeyId() {
return "test-key-" + UUID.randomUUID();
}
public static KeyGenerationRequest createTestKeyRequest() {
return KeyGenerationRequest.builder()
.algorithm("ML-KEM-768")
.keyId(generateTestKeyId())
.build();
}
}
Cleanup After Tests
Always delete test keys to avoid quota exhaustion:
@AfterEach
void cleanup() {
// Delete all test keys created during test
testKeyIds.forEach(keyId -> {
try {
client.deleteKey(keyId);
} catch (Exception e) {
// Log but don't fail cleanup
System.err.println("Failed to cleanup key: " + keyId);
}
});
}
Or use JUnit 5 TestInstance:
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class AnkaSecureIntegrationTest {
private List<String> createdKeys = new ArrayList<>();
@AfterAll
void cleanupAllKeys() {
createdKeys.forEach(client::deleteKey);
}
}
Mocking Best Practices
1. Mock External Dependencies Only
✅ DO: Mock AnkaSecureClient (external dependency)
❌ DON'T: Mock your own business logic (defeats purpose of testing)
// ✅ GOOD: Mock external SDK
@Mock
private AnkaSecureClient ankaSecureClient;
// ❌ BAD: Mock your own service
@Mock
private CustomerService customerService; // You should TEST this, not mock it
2. Verify Interactions
Verify SDK methods called correctly:
@Test
void testEncryptionUsesCorrectKey() {
// Given
when(ankaSecureClient.encrypt(any())).thenReturn(mockResponse);
// When
customerService.encryptCustomerData("data");
// Then: Verify correct key ID used
ArgumentCaptor<EncryptRequest> captor = ArgumentCaptor.forClass(EncryptRequest.class);
verify(ankaSecureClient).encrypt(captor.capture());
EncryptRequest request = captor.getValue();
assertEquals("customer-data-key", request.getKeyId());
assertEquals("data", request.getPlaintext());
}
3. Test Error Handling
Mock SDK exceptions:
@Test
void testHandlesRateLimiting() {
// Given: SDK throws rate limit exception
when(ankaSecureClient.encrypt(any()))
.thenThrow(new RateLimitException("RATE_001", "Rate limit exceeded", 60));
// When/Then: Service should retry with backoff
assertDoesNotThrow(() -> {
customerService.encryptWithRetry("data");
});
// Verify retried multiple times
verify(ankaSecureClient, atLeast(2)).encrypt(any());
}
Performance Testing
Load Testing
Test concurrent operations:
import java.util.concurrent.*;
@Test
void testConcurrentEncryption() throws InterruptedException {
int concurrentRequests = 100;
ExecutorService executor = Executors.newFixedThreadPool(10);
CountDownLatch latch = new CountDownLatch(concurrentRequests);
List<Future<EncryptResponse>> futures = new ArrayList<>();
// Submit 100 encryption requests concurrently
for (int i = 0; i < concurrentRequests; i++) {
final String data = "Data-" + i;
futures.add(executor.submit(() -> {
try {
EncryptRequest request = EncryptRequest.builder()
.keyId("concurrent-test-key")
.plaintext(data)
.build();
return client.encrypt(request);
} finally {
latch.countDown();
}
}));
}
// Wait for all requests to complete
boolean completed = latch.await(60, TimeUnit.SECONDS);
assertTrue(completed, "All requests should complete within 60 seconds");
// Verify all succeeded
long successCount = futures.stream()
.map(f -> {
try { return f.get(); }
catch (Exception e) { return null; }
})
.filter(Objects::nonNull)
.count();
assertEquals(concurrentRequests, successCount, "All requests should succeed");
executor.shutdown();
}
Benchmark Tests
Measure throughput:
@Test
void benchmarkEncryptionThroughput() {
String plaintext = "A".repeat(5 * 1024 * 1024); // 5 MB payload
int iterations = 10;
long startTime = System.currentTimeMillis();
for (int i = 0; i < iterations; i++) {
EncryptRequest request = EncryptRequest.builder()
.keyId("benchmark-key")
.plaintext(plaintext)
.build();
client.encrypt(request);
}
long duration = System.currentTimeMillis() - startTime;
double throughputMBps = (5.0 * iterations) / (duration / 1000.0);
System.out.println("Throughput: " + throughputMBps + " MB/s");
// Assert reasonable performance (adjust based on your SLA)
assertTrue(throughputMBps > 50, "Throughput should exceed 50 MB/s");
}
Test Fixtures
Reusable Test Data
public class AnkaSecureTestFixtures {
public static final String TEST_KEY_ID = "test-ml-kem-key";
public static final String TEST_ALGORITHM = "ML-KEM-768";
public static ClientConfig createTestConfig() {
return ClientConfig.builder()
.baseUrl(System.getenv("ANKASECURE_TEST_URL"))
.apiKey(System.getenv("ANKASECURE_TEST_API_KEY"))
.tenant(System.getenv("ANKASECURE_TEST_TENANT"))
.build();
}
public static KeyGenerationRequest createTestKeyRequest() {
return KeyGenerationRequest.builder()
.algorithm(TEST_ALGORITHM)
.keyId(TEST_KEY_ID + "-" + UUID.randomUUID())
.build();
}
public static EncryptRequest createEncryptRequest(String keyId, String plaintext) {
return EncryptRequest.builder()
.keyId(keyId)
.plaintext(plaintext)
.build();
}
}
Usage:
@Test
void testEncryption() {
AnkaSecureClient client = new AnkaSecureClient(
AnkaSecureTestFixtures.createTestConfig()
);
KeyResponse key = client.generateKey(
AnkaSecureTestFixtures.createTestKeyRequest()
);
// ... test logic
}
Integration Test Best Practices
1. Use Dedicated Test Tenant
✅ DO: Create separate test tenant for integration tests
❌ DON'T: Use production tenant for tests (risk of data corruption, quota exhaustion)
Setup: - Request test tenant from AnkaTech: support@ankatech.co - Configure separate credentials (test API key, test tenant ID) - Higher quotas for test tenant (unlimited or high limit)
2. Cleanup After Tests
✅ DO: Delete test keys after each test
❌ DON'T: Leave test keys accumulating (quota exhaustion, confusion)
3. Tag Integration Tests
Separate unit tests from integration tests:
import org.junit.jupiter.api.Tag;
@Tag("integration")
@Test
void testRealAPIEncryption() {
// Integration test code
}
Run only unit tests (fast):
Run integration tests (slow):
Test Scenarios
Scenario 1: Happy Path
Test successful encrypt → decrypt cycle:
@Test
void testSuccessfulEncryptDecryptCycle() {
// Generate key
KeyResponse key = client.generateKey(testKeyRequest);
// Encrypt
EncryptResponse encrypted = client.encrypt(
EncryptRequest.builder()
.keyId(key.getKeyId())
.plaintext("Test data")
.build()
);
// Decrypt
DecryptResponse decrypted = client.decrypt(
DecryptRequest.builder()
.ciphertext(encrypted.getCiphertext())
.build()
);
// Verify
assertEquals("Test data", decrypted.getPlaintext());
}
Scenario 2: Error Handling
Test authentication failure:
@Test
void testInvalidAPIKey() {
// Invalid API key
ClientConfig invalidConfig = ClientConfig.builder()
.baseUrl("https://api.ankasecure.com")
.apiKey("invalid-key")
.tenant("test-tenant")
.build();
AnkaSecureClient invalidClient = new AnkaSecureClient(invalidConfig);
// Expect 401 Unauthorized
assertThrows(UnauthorizedException.class, () -> {
invalidClient.generateKey(testKeyRequest);
});
}
Test key not found:
@Test
void testEncryptWithNonExistentKey() {
EncryptRequest request = EncryptRequest.builder()
.keyId("non-existent-key")
.plaintext("Test data")
.build();
// Expect 404 Not Found
assertThrows(KeyNotFoundException.class, () -> {
client.encrypt(request);
});
}
Test rate limiting:
@Test
void testRateLimiting() {
// Send requests until rate limit exceeded
int requestCount = 0;
boolean rateLimitHit = false;
try {
for (int i = 0; i < 100; i++) {
client.encrypt(testEncryptRequest);
requestCount++;
}
} catch (RateLimitException e) {
rateLimitHit = true;
assertEquals("RATE_001", e.getErrorCode());
assertTrue(e.getRetryAfter() > 0);
}
assertTrue(rateLimitHit || requestCount >= 60,
"Should hit rate limit or complete all requests");
}
Scenario 3: Retry Logic
Test exponential backoff:
@Test
void testRetryWithExponentialBackoff() {
AtomicInteger attemptCount = new AtomicInteger(0);
// Mock SDK to fail twice, then succeed
when(ankaSecureClient.encrypt(any()))
.then(invocation -> {
int attempt = attemptCount.incrementAndGet();
if (attempt < 3) {
throw new RateLimitException("RATE_001", "Rate limit exceeded", 60);
}
return mockEncryptResponse;
});
// Call service with retry logic
EncryptResponse response = customerService.encryptWithRetry("data");
// Verify retried 3 times total
assertEquals(3, attemptCount.get());
assertNotNull(response);
}
Testing with TestContainers (Optional)
For on-premise testing with Docker:
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
@Testcontainers
public class AnkaSecureContainerTest {
@Container
private static GenericContainer<?> ankaSecure = new GenericContainer<>("ankatech/ankasecure:3.0.0")
.withExposedPorts(8443)
.withEnv("TENANT_ID", "test-tenant")
.withEnv("API_KEY", "test-api-key");
@Test
void testWithLocalContainer() {
String baseUrl = "https://localhost:" + ankaSecure.getMappedPort(8443);
ClientConfig config = ClientConfig.builder()
.baseUrl(baseUrl)
.apiKey("test-api-key")
.tenant("test-tenant")
.build();
AnkaSecureClient client = new AnkaSecureClient(config);
// Test against local container
// ...
}
}
Note: TestContainers require Docker installed locally.
Continuous Integration
GitHub Actions Example
```.github/workflows/test.yml name: AnkaSecure Integration Tests
on: [push, pull_request]
jobs: test: runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
- name: Run Unit Tests
run: mvn test
- name: Run Integration Tests
env:
ANKASECURE_API_KEY_TEST: ${{ secrets.ANKASECURE_API_KEY_TEST }}
ANKASECURE_TENANT_ID_TEST: ${{ secrets.ANKASECURE_TENANT_ID_TEST }}
run: mvn test -Dgroups=integration
- name: Upload Test Report
if: always()
uses: actions/upload-artifact@v3
with:
name: test-results
path: target/surefire-reports/
``
**GitHub Secrets** (add in repository settings):
-ANKASECURE_API_KEY_TEST: Test tenant API key
-ANKASECURE_TENANT_ID_TEST`: Test tenant ID
Test Coverage
Recommended Coverage Targets
Unit Tests:
- Code coverage: >80%
- Branch coverage: >70%
- Critical paths: 100% (encryption, decryption, authentication)
Integration Tests:
- API coverage: >90% (test all endpoints you use)
- Error scenarios: Common errors (401, 404, 422, 429)
Measure Coverage (JaCoCo):
xml
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.11</version>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
Related Resources
- SDK Overview - Complete SDK documentation
- Performance Guide - Optimization patterns
- Troubleshooting - Common SDK errors
- Integration Flows - 28 code examples
Documentation Version: 3.0.0 Last Updated: 2025-12-26