Skip to content

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

       /\
      /  \     E2E Tests (5%)
     /────\    Integration Tests (15%)
    /──────\   Unit Tests (80%)
   /────────\

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):

ANKASECURE_API_KEY_TEST=ask_test_1234567890abcdef...
ANKASECURE_TENANT_ID_TEST=test-tenant

Add to .gitignore:

.env.test
application-test.yml


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

@AfterEach
void cleanup() {
    createdKeys.forEach(client::deleteKey);
    createdKeys.clear();
}

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):

mvn test

Run integration tests (slow):

mvn test -Dgroups=integration


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

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>



Documentation Version: 3.0.0 Last Updated: 2025-12-26