Test Compatibility Kit (TCK) for Storage Implementations
The Test Compatibility Kit (TCK) is a standardized test suite for validating storage implementations in the Fiber Storage repository. It provides a comprehensive set of tests that ensure all storage backends behave consistently and correctly implement the storage.Storage
interface.
Overviewβ
The TCK leverages testify/suite to provide a structured testing approach with setup/teardown hooks and consistent test execution. It automatically tests all core storage operations including:
- Basic CRUD operations (Set, Get, Delete)
- Context-aware operations (SetWithContext, GetWithContext, etc.)
- TTL (Time-To-Live) functionality
- Storage reset and cleanup
- Connection handling for stores that implement
StorageWithConn
Why Use the TCK?β
- Consistency: Ensures all storage implementations behave identically
- Completeness: Tests all required storage interface methods
- Maintenance: Reduces test code duplication across storage implementations
- Quality: Provides comprehensive edge case and error condition testing
- Integration: Works seamlessly with testcontainers for isolated testing
Core Conceptsβ
TCKSuite Interfaceβ
To use the TCK, you must implement the TCKSuite
interface:
// TCKSuite is the interface that must be implemented by the test suite.
// It defines how to create a new store with a container.
// The generic parameters are the storage type, the driver type returned by the Conn method,
// and the container type used to back the storage.
//
// IMPORTANT: The container type must exist as a Testcontainers module.
// Please refer to the [testcontainers] package for more information.
type TCKSuite[T storage.Storage, D any, C testcontainers.Container] interface {
// NewStore is a function that returns a new store.
// It is called by the [New] function to create a new store.
NewStore() func(ctx context.Context, tb testing.TB, ctr C) (T, error)
// NewContainer is a function that returns a new container.
// It is called by the [New] function to create a new container.
NewContainer() func(ctx context.Context, tb testing.TB) (C, error)
}
Generic Parameters:
T
: Your concrete storage type (e.g.,*mysql.Storage
)D
: The driver type returned byConn()
method (e.g.,*sql.DB
)C
: The testcontainer type (e.g.,*mysql.MySQLContainer
)
Please verify that a suitable Testcontainers module exists for your container type. See the Testcontainers modules catalog for details.
Test Execution Modesβ
The TCK supports two execution modes:
- PerTest (default): Creates a new container and storage instance for each test
- PerSuite: Creates one container and storage instance for the entire test suite
Implementation Guide: Exampleβ
Here's how to implement TCK tests for a new storage backend:
Step 1: Define Your TCK Implementationβ
// ExampleStorageTCK is the test suite for the Example storage.
type ExampleStorageTCK struct{}
// NewStore is a function that returns a new Example storage.
// It implements the [tck.TCKSuite] interface, allowing the TCK to create a new Example storage
// from the container created by the TCK.
func (s *ExampleStorageTCK) NewStore() func(ctx context.Context, tb testing.TB, ctr *ExampleContainer) (*Storage, error) {
return func(ctx context.Context, tb testing.TB, ctr *example.Container) (*Storage, error) {
// Use container APIs to get connection details
conn, err := ctr.ConnectionString(ctx)
require.NoError(tb, err)
store := New(Config{
// Apply the storage-specific configuration
ConnectionURI: conn,
Reset: true,
})
return store, nil
}
}
// NewContainer is a function that returns a new Example container.
// It implements the [tck.TCKSuite] interface, allowing the TCK to create a new Example container
// for the Example storage.
func (s *ExampleStorageTCK) NewContainer() func(ctx context.Context, tb testing.TB) (*example.Container, error) {
return func(ctx context.Context, tb testing.TB) (*example.Container, error) {
return mustStartExample(tb), nil
}
}
Step 2: Implement Container Creationβ
Create a helper function to start your storage backend's container:
func mustStartExample(t testing.TB) *example.Container {
img := exampleImage
if imgFromEnv := os.Getenv(exampleImageEnvVar); imgFromEnv != "" {
img = imgFromEnv
}
ctx := context.Background()
c, err := example.Run(ctx, img,
example.WithOptionA("valueA"),
example.WithOptionB("valueB"),
testcontainers.WithWaitStrategy(
wait.ForListeningPort("examplePort/tcp"),
),
)
testcontainers.CleanupContainer(t, c)
require.NoError(t, err)
return c
}
Step 3: Create and Run the TCK Testβ
func TestExampleStorageTCK(t *testing.T) {
// Create the TCK suite with proper generic type parameters
s, err := tck.New[*ExampleStorage, *ExampleDriver, *ExampleContainer](
context.Background(),
t,
&ExampleStorageTCK{},
tck.PerTest(), // or tck.PerSuite() for suite-level containers
)
require.NoError(t, err)
// Run all TCK tests
suite.Run(t, s)
}
Key Implementation Guidelinesβ
1. Generic Type Parametersβ
When calling tck.New
, specify the correct type parameters:
T
: Your storage pointer type (e.g.,*Storage
)D
: The driver type returned byConn()
(orany
if not applicable)C
: The container type returned byNewContainer()
2. Error Handlingβ
Always use require.NoError(tb, err)
in your factory functions to ensure test failures are properly reported.
3. Container Cleanupβ
The TCK handles container cleanup, but ensure your mustStart*
helpers call testcontainers.CleanupContainer(t, container)
. For adβhoc tests outside the TCK, call CleanupContainer
to avoid leaving containers running until the test process exits. Although Ryuk will prune them, itβs better to clean up immediately.
4. Configurationβ
Configure your storage with appropriate test settings:
- Enable
Reset: true
if your storage supports it - Use test-specific database/namespace names
- Configure appropriate timeouts and connection limits
5. Context Handlingβ
Always respect the provided context.Context
in your factory functions, especially for container startup and storage initialization.
Testing Different Scenariosβ
PerTest Mode (Recommended)β
Use when you need complete isolation between tests:
s, err := tck.New[*Storage, *sql.DB](ctx, t, &ExampleStorageTCK{}, tck.PerTest())
Pros:
- Complete test isolation
- No cross-test contamination
- Easier debugging of individual test failures
Cons:
- Slower execution due to container startup overhead
- Higher resource usage, although mitigated by Testcontainers' cleanup mechanism
PerSuite Modeβ
Use when container startup is expensive and tests can share state:
s, err := tck.New[*Storage, *sql.DB](ctx, t, &ExampleStorageTCK{}, tck.PerSuite())
Pros:
- Faster execution
- Lower resource usage
Cons:
- Tests may affect each other
- Requires careful state management
Troubleshootingβ
Common Issuesβ
- Wrong Generic Types: Ensure type parameters match your actual storage and driver types
- Container Startup Failures: Check wait strategies and ensure proper service readiness
- Connection Issues: Verify connection strings and authentication in your
NewStore()
implementation - Test Isolation: If tests interfere with each other, consider switching from
PerSuite
toPerTest
Best Practicesβ
- Use environment variables for container image versions
- Implement proper wait strategies for container readiness
- Include cleanup calls even though TCK handles them automatically
- Test your TCK implementation with both
PerTest
andPerSuite
modes - Use meaningful test data that won't conflict across parallel test runs
Complete Example Templateβ
Here's a complete template for implementing TCK tests for a new storage backend:
package newstorage
import (
"context"
"os"
"testing"
"github.com/gofiber/storage/testhelpers/tck"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"github.com/testcontainers/testcontainers-go"
// Import your specific testcontainer module
)
const (
defaultImage = "your-storage-image:latest"
imageEnvVar = "TEST_YOUR_STORAGE_IMAGE"
)
type YourStorageTCK struct{}
func (s *YourStorageTCK) NewStore() func(ctx context.Context, tb testing.TB, ctr *YourContainer) (*Storage, error) {
return func(ctx context.Context, tb testing.TB, ctr *YourContainer) (*Storage, error) {
// Get connection details from container
conn, err := ctr.ConnectionString(ctx)
require.NoError(tb, err)
// Create and configure your storage
store := New(Config{
ConnectionURI: conn,
Reset: true,
// Add other test-specific configuration
})
return store, nil
}
}
func (s *YourStorageTCK) NewContainer() func(ctx context.Context, tb testing.TB) (*YourContainer, error) {
return func(ctx context.Context, tb testing.TB) (*YourContainer, error) {
return mustStartYourStorage(tb), nil
}
}
func mustStartYourStorage(t testing.TB) *YourContainer {
img := defaultImage
if imgFromEnv := os.Getenv(imageEnvVar); imgFromEnv != "" {
img = imgFromEnv
}
ctx := context.Background()
c, err := yourstorage.Run(ctx, img,
// Add your storage-specific configuration
testcontainers.WithWaitStrategy(
// Add appropriate wait strategies
),
)
testcontainers.CleanupContainer(t, c)
require.NoError(t, err)
return c
}
func TestYourStorageTCK(t *testing.T) {
s, err := tck.New[*Storage, YourDriverType, *YourContainer](
context.Background(),
t,
&YourStorageTCK{},
tck.PerTest(),
)
require.NoError(t, err)
suite.Run(t, s)
}
This template provides a solid foundation for implementing TCK tests for any new storage backend in the Fiber Storage repository.