Skip to content

Commit a72f7d4

Browse files
1 parent 68043ae commit a72f7d4

File tree

1 file changed

+306
-0
lines changed

1 file changed

+306
-0
lines changed
Lines changed: 306 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
1+
# Test Compatibility Kit (TCK) for Storage Implementations
2+
3+
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.
4+
5+
## Overview
6+
7+
The TCK leverages [testify/suite](https://github.com/stretchr/testify#suite-package) to provide a structured testing approach with setup/teardown hooks and consistent test execution. It automatically tests all core storage operations including:
8+
9+
- Basic CRUD operations (Set, Get, Delete)
10+
- Context-aware operations (SetWithContext, GetWithContext, etc.)
11+
- TTL (Time-To-Live) functionality
12+
- Storage reset and cleanup
13+
- Connection handling for stores that implement `StorageWithConn`
14+
15+
## Why Use the TCK?
16+
17+
- **Consistency**: Ensures all storage implementations behave identically
18+
- **Completeness**: Tests all required storage interface methods
19+
- **Maintenance**: Reduces test code duplication across storage implementations
20+
- **Quality**: Provides comprehensive edge case and error condition testing
21+
- **Integration**: Works seamlessly with testcontainers for isolated testing
22+
23+
## Core Concepts
24+
25+
### TCKSuite Interface
26+
27+
To use the TCK, you must implement the `TCKSuite` interface:
28+
29+
```go
30+
// TCKSuite is the interface that must be implemented by the test suite.
31+
// It defines how to create a new store with a container.
32+
// The generic parameters are the storage type, the driver type returned by the Conn method,
33+
// and the container type used to back the storage.
34+
//
35+
// IMPORTANT: The container type must exist as a Testcontainers module.
36+
// Please refer to the [testcontainers] package for more information.
37+
type TCKSuite[T storage.Storage, D any, C testcontainers.Container] interface {
38+
// NewStore is a function that returns a new store.
39+
// It is called by the [New] function to create a new store.
40+
NewStore() func(ctx context.Context, tb testing.TB, ctr C) (T, error)
41+
42+
// NewContainer is a function that returns a new container.
43+
// It is called by the [New] function to create a new container.
44+
NewContainer() func(ctx context.Context, tb testing.TB) (C, error)
45+
}
46+
```
47+
48+
**Generic Parameters:**
49+
- `T`: Your concrete storage type (e.g., `*mysql.Storage`)
50+
- `D`: The driver type returned by `Conn()` method (e.g., `*sql.DB`)
51+
- `C`: The testcontainer type (e.g., `*mysql.MySQLContainer`)
52+
53+
Please verify that a suitable Testcontainers module exists for your container type. See the [Testcontainers modules catalog](https://testcontainers.com/modules/?language=go) for details.
54+
55+
### Test Execution Modes
56+
57+
The TCK supports two execution modes:
58+
59+
- **PerTest** (default): Creates a new container and storage instance for each test
60+
- **PerSuite**: Creates one container and storage instance for the entire test suite
61+
62+
## Implementation Guide: Example
63+
64+
Here's how to implement TCK tests for a new storage backend:
65+
66+
### Step 1: Define Your TCK Implementation
67+
68+
```go
69+
// ExampleStorageTCK is the test suite for the Example storage.
70+
type ExampleStorageTCK struct{}
71+
72+
// NewStore is a function that returns a new Example storage.
73+
// It implements the [tck.TCKSuite] interface, allowing the TCK to create a new Example storage
74+
// from the container created by the TCK.
75+
func (s *ExampleStorageTCK) NewStore() func(ctx context.Context, tb testing.TB, ctr *ExampleContainer) (*Storage, error) {
76+
return func(ctx context.Context, tb testing.TB, ctr *example.Container) (*Storage, error) {
77+
// Use container APIs to get connection details
78+
conn, err := ctr.ConnectionString(ctx)
79+
require.NoError(tb, err)
80+
81+
store := New(Config{
82+
// Apply the storage-specific configuration
83+
ConnectionURI: conn,
84+
Reset: true,
85+
})
86+
87+
return store, nil
88+
}
89+
}
90+
91+
// NewContainer is a function that returns a new Example container.
92+
// It implements the [tck.TCKSuite] interface, allowing the TCK to create a new Example container
93+
// for the Example storage.
94+
func (s *ExampleStorageTCK) NewContainer() func(ctx context.Context, tb testing.TB) (*example.Container, error) {
95+
return func(ctx context.Context, tb testing.TB) (*example.Container, error) {
96+
return mustStartExample(tb), nil
97+
}
98+
}
99+
```
100+
101+
### Step 2: Implement Container Creation
102+
103+
Create a helper function to start your storage backend's container:
104+
105+
```go
106+
func mustStartExample(t testing.TB) *example.Container {
107+
img := exampleImage
108+
if imgFromEnv := os.Getenv(exampleImageEnvVar); imgFromEnv != "" {
109+
img = imgFromEnv
110+
}
111+
112+
ctx := context.Background()
113+
114+
c, err := example.Run(ctx, img,
115+
example.WithOptionA("valueA"),
116+
example.WithOptionB("valueB"),
117+
testcontainers.WithWaitStrategy(
118+
wait.ForListeningPort("examplePort/tcp"),
119+
),
120+
)
121+
testcontainers.CleanupContainer(t, c)
122+
require.NoError(t, err)
123+
124+
return c
125+
}
126+
```
127+
128+
### Step 3: Create and Run the TCK Test
129+
130+
```go
131+
func TestExampleStorageTCK(t *testing.T) {
132+
// Create the TCK suite with proper generic type parameters
133+
s, err := tck.New[*ExampleStorage, *ExampleDriver, *ExampleContainer](
134+
context.Background(),
135+
t,
136+
&ExampleStorageTCK{},
137+
tck.PerTest(), // or tck.PerSuite() for suite-level containers
138+
)
139+
require.NoError(t, err)
140+
141+
// Run all TCK tests
142+
suite.Run(t, s)
143+
}
144+
```
145+
146+
## Key Implementation Guidelines
147+
148+
### 1. Generic Type Parameters
149+
150+
When calling `tck.New`, specify the correct type parameters:
151+
- `T`: Your storage pointer type (e.g., `*Storage`)
152+
- `D`: The driver type returned by `Conn()` (or `any` if not applicable)
153+
- `C`: The container type returned by `NewContainer()`
154+
155+
### 2. Error Handling
156+
157+
Always use `require.NoError(tb, err)` in your factory functions to ensure test failures are properly reported.
158+
159+
### 3. Container Cleanup
160+
161+
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.
162+
163+
### 4. Configuration
164+
165+
Configure your storage with appropriate test settings:
166+
- Enable `Reset: true` if your storage supports it
167+
- Use test-specific database/namespace names
168+
- Configure appropriate timeouts and connection limits
169+
170+
### 5. Context Handling
171+
172+
Always respect the provided `context.Context` in your factory functions, especially for container startup and storage initialization.
173+
174+
## Testing Different Scenarios
175+
176+
### PerTest Mode (Recommended)
177+
Use when you need complete isolation between tests:
178+
179+
```go
180+
s, err := tck.New[*Storage, *sql.DB](ctx, t, &ExampleStorageTCK{}, tck.PerTest())
181+
```
182+
183+
**Pros:**
184+
- Complete test isolation
185+
- No cross-test contamination
186+
- Easier debugging of individual test failures
187+
188+
**Cons:**
189+
- Slower execution due to container startup overhead
190+
- Higher resource usage, although mitigated by Testcontainers' cleanup mechanism
191+
192+
### PerSuite Mode
193+
Use when container startup is expensive and tests can share state:
194+
195+
```go
196+
s, err := tck.New[*Storage, *sql.DB](ctx, t, &ExampleStorageTCK{}, tck.PerSuite())
197+
```
198+
199+
**Pros:**
200+
- Faster execution
201+
- Lower resource usage
202+
203+
**Cons:**
204+
- Tests may affect each other
205+
- Requires careful state management
206+
207+
## Troubleshooting
208+
209+
### Common Issues
210+
211+
1. **Wrong Generic Types**: Ensure type parameters match your actual storage and driver types
212+
2. **Container Startup Failures**: Check wait strategies and ensure proper service readiness
213+
3. **Connection Issues**: Verify connection strings and authentication in your `NewStore()` implementation
214+
4. **Test Isolation**: If tests interfere with each other, consider switching from `PerSuite` to `PerTest`
215+
216+
### Best Practices
217+
218+
- Use environment variables for container image versions
219+
- Implement proper wait strategies for container readiness
220+
- Include cleanup calls even though TCK handles them automatically
221+
- Test your TCK implementation with both `PerTest` and `PerSuite` modes
222+
- Use meaningful test data that won't conflict across parallel test runs
223+
224+
## Complete Example Template
225+
226+
Here's a complete template for implementing TCK tests for a new storage backend:
227+
228+
```go
229+
package newstorage
230+
231+
import (
232+
"context"
233+
"os"
234+
"testing"
235+
236+
"github.com/gofiber/storage/testhelpers/tck"
237+
"github.com/stretchr/testify/require"
238+
"github.com/stretchr/testify/suite"
239+
"github.com/testcontainers/testcontainers-go"
240+
// Import your specific testcontainer module
241+
)
242+
243+
const (
244+
defaultImage = "your-storage-image:latest"
245+
imageEnvVar = "TEST_YOUR_STORAGE_IMAGE"
246+
)
247+
248+
type YourStorageTCK struct{}
249+
250+
func (s *YourStorageTCK) NewStore() func(ctx context.Context, tb testing.TB, ctr *YourContainer) (*Storage, error) {
251+
return func(ctx context.Context, tb testing.TB, ctr *YourContainer) (*Storage, error) {
252+
// Get connection details from container
253+
conn, err := ctr.ConnectionString(ctx)
254+
require.NoError(tb, err)
255+
256+
// Create and configure your storage
257+
store := New(Config{
258+
ConnectionURI: conn,
259+
Reset: true,
260+
// Add other test-specific configuration
261+
})
262+
263+
return store, nil
264+
}
265+
}
266+
267+
func (s *YourStorageTCK) NewContainer() func(ctx context.Context, tb testing.TB) (*YourContainer, error) {
268+
return func(ctx context.Context, tb testing.TB) (*YourContainer, error) {
269+
return mustStartYourStorage(tb), nil
270+
}
271+
}
272+
273+
func mustStartYourStorage(t testing.TB) *YourContainer {
274+
img := defaultImage
275+
if imgFromEnv := os.Getenv(imageEnvVar); imgFromEnv != "" {
276+
img = imgFromEnv
277+
}
278+
279+
ctx := context.Background()
280+
281+
c, err := yourstorage.Run(ctx, img,
282+
// Add your storage-specific configuration
283+
testcontainers.WithWaitStrategy(
284+
// Add appropriate wait strategies
285+
),
286+
)
287+
testcontainers.CleanupContainer(t, c)
288+
require.NoError(t, err)
289+
290+
return c
291+
}
292+
293+
func TestYourStorageTCK(t *testing.T) {
294+
s, err := tck.New[*Storage, YourDriverType, *YourContainer](
295+
context.Background(),
296+
t,
297+
&YourStorageTCK{},
298+
tck.PerTest(),
299+
)
300+
require.NoError(t, err)
301+
302+
suite.Run(t, s)
303+
}
304+
```
305+
306+
This template provides a solid foundation for implementing TCK tests for any new storage backend in the Fiber Storage repository.

0 commit comments

Comments
 (0)