Files
storage/testhelpers/tck
Manuel de la Peña 0d2fbb1f6c docs: refinements
2025-08-29 13:49:48 +02:00
..
2025-08-29 13:49:48 +02:00
2025-08-29 12:05:15 +02:00

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 by Conn() 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 by Conn() (or any if not applicable)
  • C: The container type returned by NewContainer()

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 adhoc tests outside the TCK, call CleanupContainer to avoid leaving containers running until the test process exits. Although Ryuk will prune them, its 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

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

  1. Wrong Generic Types: Ensure type parameters match your actual storage and driver types
  2. Container Startup Failures: Check wait strategies and ensure proper service readiness
  3. Connection Issues: Verify connection strings and authentication in your NewStore() implementation
  4. Test Isolation: If tests interfere with each other, consider switching from PerSuite to PerTest

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 and PerSuite 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.