Testing¶
Comprehensive testing strategy and guidelines for the Tileverse Range Reader library.
Testing Strategy¶
The project employs a multi-layered testing approach:
- Unit Tests (
*Test.java
): Fast, isolated tests - Integration Tests (
*IT.java
): End-to-end tests with real services - Performance Tests (
*PerformanceTest.java
): Throughput and latency analysis - Benchmarks: JMH-based comprehensive performance testing
Test Categories¶
Unit Tests¶
Fast tests that verify individual components in isolation:
# Run all unit tests (recommended)
make test-unit
# Module-specific unit tests
make test-core # Core module only
make test-s3 # S3 module only
make test-azure # Azure module only
make test-gcs # GCS module only
# Direct Maven commands for specific test classes/methods
./mvnw test -Dtest="CachingRangeReaderTest" # Specific class
./mvnw test -Dtest="CachingRangeReaderTest#testBasicCaching" # Specific method
./mvnw test -pl src/core -Dtest="FileRangeReaderTest" # Class in specific module
Example Unit Test¶
@Test
void testBasicFileReading() throws IOException {
Path testFile = Files.createTempFile("test", ".bin");
Files.write(testFile, "Hello, World!".getBytes());
try (var reader = FileRangeReader.builder()
.path(testFile)
.build()) {
ByteBuffer result = reader.readRange(0, 5);
assertEquals("Hello", new String(result.array(), 0, result.remaining()));
}
Files.deleteIfExists(testFile);
}
Integration Tests¶
End-to-end tests using TestContainers for realistic scenarios:
# Run all integration tests (recommended)
make test-it
# Module-specific integration tests
make test-core-it # Core integration tests (HTTP with Nginx)
make test-s3-it # S3 integration tests (LocalStack + MinIO)
make test-azure-it # Azure integration tests (Azurite)
make test-gcs-it # GCS integration tests
# With TestContainers reuse for faster execution
export TESTCONTAINERS_REUSE_ENABLE=true
make test-it
# Direct Maven commands for integration tests
./mvnw verify -pl src/s3 # All S3 integration tests
./mvnw verify -pl src/azure # All Azure integration tests
./mvnw verify -pl src/core # All core integration tests
./mvnw verify # All integration tests
# For specific integration test classes (rarely needed)
./mvnw test -pl src/s3 -Dtest="S3RangeReaderIT" # Specific S3 test
./mvnw test -pl src/azure -Dtest="AzureBlobRangeReaderIT" # Specific Azure test
# Note: Module-specific make targets run ALL integration tests in that module
# This is usually what you want for comprehensive testing
TestContainers Setup¶
All integration tests extend a common base class:
@Testcontainers(disabledWithoutDocker = true)
public class S3RangeReaderIT extends AbstractRangeReaderIT {
@Container
static LocalStackContainer localstack = new LocalStackContainer(
DockerImageName.parse("localstack/localstack:3.2.0"))
.withServices(LocalStackContainer.Service.S3);
@Override
protected RangeReader createBaseReader() throws IOException {
return S3RangeReader.builder()
.endpointOverride(localstack.getEndpoint())
.region(Region.of(localstack.getRegion()))
.build();
}
}
Performance Tests¶
Measure performance characteristics under various conditions:
# Run performance tests (recommended)
make perf-test
# Direct Maven commands
./mvnw test -Dtest="*PerformanceTest" # All performance tests
./mvnw test -Dtest="RangeReaderPerformanceTest" -Dperformance.iterations=1000 # With custom parameters
./mvnw test -pl src/core -Dtest="*PerformanceTest" # Module-specific
Example Performance Test¶
@Test
void testLargeFilePerformance() throws IOException {
Path largeFile = createLargeTestFile(100 * 1024 * 1024); // 100MB
try (var reader = FileRangeReader.builder()
.path(largeFile)
.build()) {
long startTime = System.nanoTime();
// Read 1000 random ranges
for (int i = 0; i < 1000; i++) {
long offset = ThreadLocalRandom.current().nextLong(largeFile.toFile().length() - 1024);
reader.readRange(offset, 1024);
}
long endTime = System.nanoTime();
double durationMs = (endTime - startTime) / 1_000_000.0;
System.out.println("1000 reads took " + durationMs + "ms");
assertTrue(durationMs < 10000, "Performance regression detected");
}
}
Base Test Classes¶
AbstractRangeReaderIT¶
All integration tests extend this base class to ensure consistent behavior:
public abstract class AbstractRangeReaderIT {
protected static final int TEST_FILE_SIZE = 10 * 1024 * 1024; // 10MB
protected abstract RangeReader createBaseReader() throws IOException;
@Test
void testBasicRangeReading() throws IOException {
try (RangeReader reader = createBaseReader()) {
ByteBuffer data = reader.readRange(0, 1024);
assertEquals(1024, data.remaining());
}
}
@Test
void testBoundaryConditions() throws IOException {
try (RangeReader reader = createBaseReader()) {
long size = reader.size();
// Test reading at EOF
ByteBuffer data = reader.readRange(size - 10, 20);
assertEquals(10, data.remaining());
// Test reading beyond EOF
ByteBuffer empty = reader.readRange(size + 100, 1024);
assertEquals(0, empty.remaining());
}
}
// More common test cases...
}
TestContainers Integration¶
Available Test Containers¶
Service | Container | Purpose |
---|---|---|
S3 | localstack/localstack:3.2.0 | AWS S3 API emulation |
MinIO | minio/minio:latest | S3-compatible storage |
Azure | mcr.microsoft.com/azure-storage/azurite:latest | Azure Blob Storage |
HTTP | nginx:alpine | HTTP server with authentication |
Container Configuration Examples¶
LocalStack (S3)¶
@Container
static LocalStackContainer localstack = new LocalStackContainer(
DockerImageName.parse("localstack/localstack:3.2.0"))
.withServices(LocalStackContainer.Service.S3)
.withEnv("DEBUG", "1");
@BeforeAll
static void setupS3() throws IOException {
S3Client s3Client = S3Client.builder()
.endpointOverride(localstack.getEndpoint())
.region(Region.of(localstack.getRegion()))
.credentialsProvider(StaticCredentialsProvider.create(
AwsBasicCredentials.create(
localstack.getAccessKey(),
localstack.getSecretKey())))
.build();
s3Client.createBucket(CreateBucketRequest.builder()
.bucket("test-bucket")
.build());
// Upload test file
s3Client.putObject(
PutObjectRequest.builder()
.bucket("test-bucket")
.key("test-file.bin")
.build(),
RequestBody.fromFile(testFile));
}
MinIO¶
@Container
static MinIOContainer minio = new MinIOContainer("minio/minio:latest");
@BeforeAll
static void setupMinIO() throws IOException {
S3Client s3Client = S3Client.builder()
.endpointOverride(URI.create(minio.getS3URL()))
.region(Region.US_EAST_1)
.credentialsProvider(StaticCredentialsProvider.create(
AwsBasicCredentials.create(
minio.getUserName(),
minio.getPassword())))
.forcePathStyle(true)
.build();
// Create bucket and upload test data
}
Azurite (Azure Blob Storage)¶
@Container
static GenericContainer<?> azurite = new GenericContainer<>("mcr.microsoft.com/azure-storage/azurite:latest")
.withExposedPorts(10000)
.withCommand("azurite-blob", "--blobHost", "0.0.0.0");
@BeforeAll
static void setupAzure() throws IOException {
String connectionString = String.format(
"DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;" +
"AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;" +
"BlobEndpoint=http://%s:%d/devstoreaccount1;",
azurite.getHost(), azurite.getMappedPort(10000));
BlobServiceClient blobClient = new BlobServiceClientBuilder()
.connectionString(connectionString)
.buildClient();
// Create container and upload test data
}
Test Utilities¶
TestUtil Class¶
Common utilities for creating test data:
public class TestUtil {
public static Path createTempTestFile(int sizeBytes) throws IOException {
Path testFile = Files.createTempFile("rangereader-test", ".bin");
// Create deterministic test data
byte[] data = new byte[sizeBytes];
Random random = new Random(42); // Fixed seed for reproducibility
random.nextBytes(data);
Files.write(testFile, data);
return testFile;
}
public static void verifyRangeContent(ByteBuffer actual, byte[] expected,
int offset, int length) {
assertEquals(length, actual.remaining());
for (int i = 0; i < length; i++) {
assertEquals(expected[offset + i], actual.get(i),
"Mismatch at position " + i);
}
}
public static byte[] generateTestData(int size, long seed) {
byte[] data = new byte[size];
Random random = new Random(seed);
random.nextBytes(data);
return data;
}
}
Test Data Management¶
Consistent Test Data¶
All tests use the same deterministic test data:
public class AbstractRangeReaderIT {
protected static final int TEST_FILE_SIZE = 10 * 1024 * 1024; // 10MB
protected static final long TEST_DATA_SEED = 42L;
protected static byte[] createExpectedData() {
return TestUtil.generateTestData(TEST_FILE_SIZE, TEST_DATA_SEED);
}
@Test
void testRangeConsistency() throws IOException {
byte[] expectedData = createExpectedData();
try (RangeReader reader = createBaseReader()) {
// Test various ranges
verifyRange(reader, expectedData, 0, 1024);
verifyRange(reader, expectedData, 5000, 2048);
verifyRange(reader, expectedData, TEST_FILE_SIZE - 1000, 1000);
}
}
private void verifyRange(RangeReader reader, byte[] expected,
int offset, int length) throws IOException {
ByteBuffer actual = reader.readRange(offset, length);
TestUtil.verifyRangeContent(actual, expected, offset, length);
}
}
Benchmarks with JMH¶
Running Benchmarks¶
# Build and run benchmarks (recommended)
make build-benchmarks # Build benchmark JAR
make benchmarks # Run all benchmarks
# Specific benchmark types
make benchmarks-file # Run file-based benchmarks only
make benchmarks-gc # Run benchmarks with GC profiling
# Build cloud benchmarks (requires TestContainers)
make benchmarks-cloud
# Direct execution
java -jar benchmarks/target/benchmarks.jar # All benchmarks
java -jar benchmarks/target/benchmarks.jar FileRangeReader # Specific benchmark
java -jar benchmarks/target/benchmarks.jar -prof gc # With profiling
java -jar benchmarks/target/benchmarks.jar -f 3 -wi 5 -i 10 # Custom parameters
Example Benchmark¶
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.SECONDS)
@State(Scope.Benchmark)
public class FileRangeReaderBenchmark {
private RangeReader reader;
private ByteBuffer buffer;
@Setup
public void setup() throws IOException {
Path testFile = TestUtil.createTempTestFile(100 * 1024 * 1024);
reader = FileRangeReader.builder()
.path(testFile)
.build();
buffer = ByteBuffer.allocate(64 * 1024);
}
@Benchmark
public int sequentialReads() throws IOException {
buffer.clear();
return reader.readRange(ThreadLocalRandom.current().nextLong(1024 * 1024),
64 * 1024, buffer);
}
@TearDown
public void tearDown() throws IOException {
reader.close();
}
}
Testing Best Practices¶
Test Organization¶
class CachingRangeReaderTest {
@Nested
@DisplayName("Basic Functionality")
class BasicFunctionality {
@Test void testCacheHit() { }
@Test void testCacheMiss() { }
}
@Nested
@DisplayName("Configuration")
class Configuration {
@Test void testMaximumSize() { }
@Test void testExpiration() { }
}
@Nested
@DisplayName("Error Handling")
class ErrorHandling {
@Test void testDelegateFailure() { }
@Test void testInvalidParameters() { }
}
}
Parameterized Tests¶
@ParameterizedTest
@ValueSource(ints = {100, 1000, 10000})
void testVariousCacheSizes(int cacheSize) throws IOException {
try (var reader = CachingRangeReader.builder(baseReader)
.maximumSize(cacheSize)
.build()) {
ByteBuffer data = reader.readRange(100, 500);
assertEquals(500, data.remaining());
}
}
Test Resource Management¶
@TempDir
Path tempDir;
@Test
void testWithTempDirectory() throws IOException {
Path testFile = tempDir.resolve("test.bin");
Files.write(testFile, "test data".getBytes());
try (var reader = FileRangeReader.builder()
.path(testFile)
.build()) {
// Test operations
}
// File automatically cleaned up by @TempDir
}
Continuous Integration¶
GitHub Actions Testing¶
The project runs comprehensive tests in CI using Makefile targets:
# .github/workflows/pr-validation.yml
jobs:
build:
strategy:
matrix:
java-version: ['17', '21', '24']
steps:
- name: Run unit tests
run: make test-unit
integration-tests:
strategy:
matrix:
java-version: ['17', '21', '24']
test-group: ['core', 's3', 'azure', 'gcs']
steps:
- name: Run integration tests
run: make test-${{ matrix.test-group }}-it
quality:
steps:
- name: Check formatting
run: make lint
- name: Full verification
run: make verify
Test Parallelization¶
# TestContainers reuse for faster integration tests (recommended)
export TESTCONTAINERS_REUSE_ENABLE=true
make test-it
# Module-specific integration tests with reuse
export TESTCONTAINERS_REUSE_ENABLE=true
make test-s3-it
# Direct Maven commands for parallel execution
./mvnw test -Dparallel=classes -DthreadCount=4 # Parallel unit tests
Debugging Tests¶
Test Logging¶
// Enable debug logging for tests
@TestMethodOrder(OrderAnnotation.class)
class DebugTest {
@BeforeEach
void setupLogging() {
System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "DEBUG");
System.setProperty("org.slf4j.simpleLogger.log.io.tileverse.rangereader", "TRACE");
}
}
IDE Test Configuration¶
IntelliJ IDEA¶
Run Configuration:
- Working directory: $MODULE_WORKING_DIR$
- VM options: -ea -Dtestcontainers.reuse.enable=true
- Environment variables: TESTCONTAINERS_REUSE_ENABLE=true
Eclipse¶
Run Configuration:
- Arguments tab → VM arguments: -ea
- Environment tab → Add: TESTCONTAINERS_REUSE_ENABLE=true
Test Coverage¶
Measuring Coverage¶
# Generate coverage report
./mvnw test jacoco:report
# View coverage report
open target/site/jacoco/index.html
Coverage Goals¶
- Line Coverage: > 85%
- Branch Coverage: > 80%
- Method Coverage: > 90%
Next Steps¶
- Performance: Learn about performance testing and optimization
- Contributing: Guidelines for contributing tests
- Building: Build system and test execution