mirror of
https://github.com/gofiber/storage.git
synced 2025-11-03 10:50:58 +08:00
chore: split storage and container creation for better granularity in the suite hook
This commit is contained in:
@@ -26,11 +26,9 @@ const (
|
||||
|
||||
type MySQLStorageTCK struct{}
|
||||
|
||||
func (s *MySQLStorageTCK) NewStoreWithContainer() func(ctx context.Context, tb testing.TB) (*Storage, testcontainers.Container, error) {
|
||||
return func(ctx context.Context, tb testing.TB) (*Storage, testcontainers.Container, error) {
|
||||
c := mustStartMySQL(tb)
|
||||
|
||||
conn, err := c.ConnectionString(ctx)
|
||||
func (s *MySQLStorageTCK) NewStore() func(ctx context.Context, tb testing.TB, ctr *mysql.MySQLContainer) (*Storage, error) {
|
||||
return func(ctx context.Context, tb testing.TB, ctr *mysql.MySQLContainer) (*Storage, error) {
|
||||
conn, err := ctr.ConnectionString(ctx)
|
||||
require.NoError(tb, err)
|
||||
|
||||
store := New(Config{
|
||||
@@ -38,7 +36,13 @@ func (s *MySQLStorageTCK) NewStoreWithContainer() func(ctx context.Context, tb t
|
||||
Reset: true,
|
||||
})
|
||||
|
||||
return store, c, nil
|
||||
return store, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MySQLStorageTCK) NewContainer() func(ctx context.Context, tb testing.TB) (*mysql.MySQLContainer, error) {
|
||||
return func(ctx context.Context, tb testing.TB) (*mysql.MySQLContainer, error) {
|
||||
return mustStartMySQL(tb), nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +52,11 @@ func newTestStore(t testing.TB) *Storage {
|
||||
ctx := context.Background()
|
||||
|
||||
suite := MySQLStorageTCK{}
|
||||
store, _, err := suite.NewStoreWithContainer()(ctx, t)
|
||||
|
||||
ctr, err := suite.NewContainer()(ctx, t)
|
||||
require.NoError(t, err)
|
||||
|
||||
store, err := suite.NewStore()(ctx, t, ctr)
|
||||
require.NoError(t, err)
|
||||
|
||||
return store
|
||||
|
||||
@@ -68,26 +68,29 @@ func PerSuite() suiteOption {
|
||||
|
||||
// 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 and the driver type returned by the Conn method.
|
||||
type TCKSuite[T storage.Storage, D any] interface {
|
||||
NewStoreWithContainer() func(ctx context.Context, tb testing.TB) (T, testcontainers.Container, error)
|
||||
// The generic parameters are the storage type, the driver type returned by the Conn method,
|
||||
// and the container type used to back the storage.
|
||||
type TCKSuite[T storage.Storage, D any, C testcontainers.Container] interface {
|
||||
NewStore() func(ctx context.Context, tb testing.TB, ctr C) (T, error)
|
||||
NewContainer() func(ctx context.Context, tb testing.TB) (C, error)
|
||||
}
|
||||
|
||||
// New creates a new [StorageTestSuite] with the given [TCKSuite].
|
||||
func New[T storage.Storage, D any](ctx context.Context, t *testing.T, tckSuite TCKSuite[T, D], opts ...suiteOption) (StorageTestSuite[T, D], error) {
|
||||
func New[T storage.Storage, D any, C testcontainers.Container](ctx context.Context, t *testing.T, tckSuite TCKSuite[T, D, C], opts ...suiteOption) (StorageTestSuite[T, D, C], error) {
|
||||
if tckSuite == nil {
|
||||
return StorageTestSuite[T, D]{}, fmt.Errorf("test suite is nil")
|
||||
return StorageTestSuite[T, D, C]{}, fmt.Errorf("test suite is nil")
|
||||
}
|
||||
|
||||
s := StorageTestSuite[T, D]{
|
||||
ctx: ctx,
|
||||
hook: perTest, // defaults to perTest
|
||||
createFn: tckSuite.NewStoreWithContainer(),
|
||||
s := StorageTestSuite[T, D, C]{
|
||||
ctx: ctx,
|
||||
hook: perTest, // defaults to perTest
|
||||
createStoreFn: tckSuite.NewStore(),
|
||||
createContainerFn: tckSuite.NewContainer(),
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
if err := opt.apply(&s); err != nil {
|
||||
return StorageTestSuite[T, D]{}, fmt.Errorf("apply option: %w", err)
|
||||
return StorageTestSuite[T, D, C]{}, fmt.Errorf("apply option: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,22 +101,48 @@ func New[T storage.Storage, D any](ctx context.Context, t *testing.T, tckSuite T
|
||||
|
||||
// StorageTestSuite is the test suite for the storage.
|
||||
// It implements the [suite.Suite] interface and provides the necessary methods to test the storage.
|
||||
// The generic parameters are the storage type and the driver type returned by the Conn method.
|
||||
type StorageTestSuite[T storage.Storage, D any] struct {
|
||||
// The generic parameters are the storage type, the driver type returned by the Conn method,
|
||||
// and the container type used to back the storage.
|
||||
type StorageTestSuite[T storage.Storage, D any, C testcontainers.Container] struct {
|
||||
// embed the suite.Suite to provide the necessary methods to define the lifecycle of the test suite.
|
||||
suite.Suite
|
||||
stats *suite.SuiteInformation
|
||||
ctx context.Context
|
||||
hook suiteHook
|
||||
createFn func(ctx context.Context, tb testing.TB) (T, testcontainers.Container, error)
|
||||
store storage.Storage
|
||||
|
||||
// stats is the statistics of the test suite.
|
||||
// It is used to store the statistics for later use.
|
||||
stats *suite.SuiteInformation
|
||||
|
||||
// ctx is the context of the test suite.
|
||||
// It is used to store the context for later use.
|
||||
ctx context.Context
|
||||
|
||||
// hook is the hook for creating the store and container.
|
||||
hook suiteHook
|
||||
|
||||
// createStoreFn is the function to create the store.
|
||||
// It's called by the [SetupSuite] and [SetupTest] hooks, depending on the suite hook value.
|
||||
createStoreFn func(ctx context.Context, tb testing.TB, ctr C) (T, error)
|
||||
|
||||
// createContainerFn is the function to create the container.
|
||||
// It's called by the [SetupSuite] and [SetupTest] hooks, depending on the suite hook value.
|
||||
createContainerFn func(ctx context.Context, tb testing.TB) (C, error)
|
||||
|
||||
// store is the store under test.
|
||||
store storage.Storage
|
||||
|
||||
// closedStore is a flag to check if the store is closed.
|
||||
closedStore bool
|
||||
mu sync.Mutex
|
||||
ctr testcontainers.Container
|
||||
|
||||
// closedStoreMu is the mutex for the closedStore flag.
|
||||
closedStoreMu sync.Mutex
|
||||
|
||||
// ctr is the container backing the store under test.
|
||||
ctr C
|
||||
}
|
||||
|
||||
func (s *StorageTestSuite[T, D]) updateHook(hook suiteHook) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
// updateHook is a helper function to update the hook for creating the store and container.
|
||||
func (s *StorageTestSuite[T, D, C]) updateHook(hook suiteHook) error {
|
||||
s.closedStoreMu.Lock()
|
||||
defer s.closedStoreMu.Unlock()
|
||||
|
||||
s.hook = hook
|
||||
|
||||
@@ -122,15 +151,15 @@ func (s *StorageTestSuite[T, D]) updateHook(hook suiteHook) error {
|
||||
|
||||
// cleanup is a helper function to cleanup the store and container.
|
||||
// To avoid double closing the store, it checks if the store is already closed.
|
||||
func (s *StorageTestSuite[T, D]) cleanup() error {
|
||||
func (s *StorageTestSuite[T, D, C]) cleanup() error {
|
||||
t := s.T()
|
||||
t.Log("🧹 Cleaning up store and container")
|
||||
|
||||
var err error
|
||||
|
||||
if s.store != nil {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.closedStoreMu.Lock()
|
||||
defer s.closedStoreMu.Unlock()
|
||||
|
||||
if !s.closedStore {
|
||||
err = s.store.Close()
|
||||
@@ -150,22 +179,23 @@ func (s *StorageTestSuite[T, D]) cleanup() error {
|
||||
|
||||
// HandleStats is a hook that is called when the suite statistics are updated.
|
||||
// It is used to store the statistics for later use.
|
||||
func (s *StorageTestSuite[T, D]) HandleStats(_ string, stats *suite.SuiteInformation) {
|
||||
func (s *StorageTestSuite[T, D, C]) HandleStats(_ string, stats *suite.SuiteInformation) {
|
||||
s.stats = stats
|
||||
}
|
||||
|
||||
// SetupSuite is a hook that is called when the suite is setup.
|
||||
// It is used to create the store and container, only if the creation hook is [PerSuite].
|
||||
func (s *StorageTestSuite[T, D]) SetupSuite() {
|
||||
func (s *StorageTestSuite[T, D, C]) SetupSuite() {
|
||||
if s.hook == perSuite {
|
||||
t := s.T()
|
||||
|
||||
store, ctr, err := s.createFn(s.ctx, t)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create store per suite: %v", err)
|
||||
}
|
||||
s.store = store
|
||||
ctr, err := s.createContainerFn(s.ctx, t)
|
||||
s.Require().NoError(err)
|
||||
s.ctr = ctr
|
||||
|
||||
store, err := s.createStoreFn(s.ctx, t, ctr)
|
||||
s.Require().NoError(err)
|
||||
s.store = store
|
||||
s.closedStore = false
|
||||
|
||||
err = s.store.Reset()
|
||||
@@ -175,7 +205,7 @@ func (s *StorageTestSuite[T, D]) SetupSuite() {
|
||||
|
||||
// TearDownSuite is a hook that is called when the suite is torn down.
|
||||
// It is used to cleanup the store and container, only if the creation hook is [PerSuite].
|
||||
func (s *StorageTestSuite[T, D]) TearDownSuite() {
|
||||
func (s *StorageTestSuite[T, D, C]) TearDownSuite() {
|
||||
if s.hook == perSuite {
|
||||
s.Require().NoError(s.cleanup())
|
||||
}
|
||||
@@ -183,26 +213,41 @@ func (s *StorageTestSuite[T, D]) TearDownSuite() {
|
||||
|
||||
// SetupTest is a hook that is called when the test is setup.
|
||||
// It is used to create the store and container, only if the creation hook is [PerTest].
|
||||
func (s *StorageTestSuite[T, D]) SetupTest() {
|
||||
if s.hook == perTest {
|
||||
t := s.T()
|
||||
func (s *StorageTestSuite[T, D, C]) SetupTest() {
|
||||
t := s.T()
|
||||
|
||||
store, ctr, err := s.createFn(s.ctx, t)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create store per test: %v", err)
|
||||
}
|
||||
s.store = store
|
||||
switch s.hook {
|
||||
case perTest:
|
||||
ctr, err := s.createContainerFn(s.ctx, t)
|
||||
s.Require().NoError(err)
|
||||
s.ctr = ctr
|
||||
|
||||
store, err := s.createStoreFn(s.ctx, t, ctr)
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.store = store
|
||||
s.closedStore = false
|
||||
|
||||
err = s.store.Reset()
|
||||
s.Require().NoError(err)
|
||||
case perSuite:
|
||||
// update the store with the container from the suite
|
||||
// This prevents the error caused by closing the store
|
||||
// in a test and using it in a different test of the
|
||||
// same suite when the hook is [PerSuite].
|
||||
store, err := s.createStoreFn(s.ctx, t, s.ctr)
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.store = store
|
||||
s.closedStore = false
|
||||
default:
|
||||
s.T().Fatalf("invalid hook: %d", s.hook)
|
||||
}
|
||||
}
|
||||
|
||||
// TearDownTest is a hook that is called when the test is torn down.
|
||||
// It is used to cleanup the store and container, only if the creation hook is [PerTest].
|
||||
func (s *StorageTestSuite[T, D]) TearDownTest() {
|
||||
func (s *StorageTestSuite[T, D, C]) TearDownTest() {
|
||||
if s.hook == perTest {
|
||||
s.Require().NoError(s.cleanup())
|
||||
}
|
||||
@@ -212,7 +257,7 @@ func (s *StorageTestSuite[T, D]) TearDownTest() {
|
||||
// Tests
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
func (s *StorageTestSuite[T, D]) TestConn() {
|
||||
func (s *StorageTestSuite[T, D, C]) TestConn() {
|
||||
storeWithConn, ok := s.store.(storage.StorageWithConn[D])
|
||||
if !ok {
|
||||
s.T().Skip("Storage does not implement StorageWithConn")
|
||||
@@ -223,11 +268,11 @@ func (s *StorageTestSuite[T, D]) TestConn() {
|
||||
s.Require().NotNil(conn, "Conn should not be nil")
|
||||
}
|
||||
|
||||
func (s *StorageTestSuite[T, D]) TestSet() {
|
||||
func (s *StorageTestSuite[T, D, C]) TestSet() {
|
||||
s.setValue("test_key", []byte("test_value"))
|
||||
}
|
||||
|
||||
func (s *StorageTestSuite[T, D]) TestSetWithContext() {
|
||||
func (s *StorageTestSuite[T, D, C]) TestSetWithContext() {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
@@ -235,19 +280,19 @@ func (s *StorageTestSuite[T, D]) TestSetWithContext() {
|
||||
s.Require().ErrorIs(err, context.Canceled)
|
||||
}
|
||||
|
||||
func (s *StorageTestSuite[T, D]) TestSetAndOverride() {
|
||||
func (s *StorageTestSuite[T, D, C]) TestSetAndOverride() {
|
||||
s.setValue("test_key", []byte("test_value"))
|
||||
s.setValue("test_key", []byte("test_value_2"))
|
||||
|
||||
s.requireKeyHasValue("test_key", []byte("test_value_2"))
|
||||
}
|
||||
|
||||
func (s *StorageTestSuite[T, D]) TestSetAndGet() {
|
||||
func (s *StorageTestSuite[T, D, C]) TestSetAndGet() {
|
||||
s.setValue("test_key", []byte("test_value"))
|
||||
s.requireKeyHasValue("test_key", []byte("test_value"))
|
||||
}
|
||||
|
||||
func (s *StorageTestSuite[T, D]) TestGetWithContext() {
|
||||
func (s *StorageTestSuite[T, D, C]) TestGetWithContext() {
|
||||
s.setValue("test_key", []byte("test_value"))
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
@@ -258,13 +303,13 @@ func (s *StorageTestSuite[T, D]) TestGetWithContext() {
|
||||
s.Require().Zero(len(result))
|
||||
}
|
||||
|
||||
func (s *StorageTestSuite[T, D]) TestGetMissing() {
|
||||
func (s *StorageTestSuite[T, D, C]) TestGetMissing() {
|
||||
val, err := s.store.Get("non-existent-key")
|
||||
s.Require().NoError(err)
|
||||
s.Require().Zero(len(val))
|
||||
}
|
||||
|
||||
func (s *StorageTestSuite[T, D]) TestGetExpired() {
|
||||
func (s *StorageTestSuite[T, D, C]) TestGetExpired() {
|
||||
s.setValueWithTTL("temp_key", []byte("temp_value"), 500*time.Millisecond)
|
||||
|
||||
s.Eventually(func() bool {
|
||||
@@ -273,7 +318,7 @@ func (s *StorageTestSuite[T, D]) TestGetExpired() {
|
||||
}, 2*time.Second, 1*time.Second, "Key should expire")
|
||||
}
|
||||
|
||||
func (s *StorageTestSuite[T, D]) TestDelete() {
|
||||
func (s *StorageTestSuite[T, D, C]) TestDelete() {
|
||||
s.setValue("delete_me", []byte("delete_value"))
|
||||
|
||||
err := s.store.Delete("delete_me")
|
||||
@@ -282,7 +327,7 @@ func (s *StorageTestSuite[T, D]) TestDelete() {
|
||||
s.requireKeyNotExists("delete_me")
|
||||
}
|
||||
|
||||
func (s *StorageTestSuite[T, D]) TestDeleteWithContext() {
|
||||
func (s *StorageTestSuite[T, D, C]) TestDeleteWithContext() {
|
||||
s.setValue("delete_me", []byte("delete_value"))
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
@@ -296,7 +341,7 @@ func (s *StorageTestSuite[T, D]) TestDeleteWithContext() {
|
||||
s.Require().Equal([]byte("delete_value"), result)
|
||||
}
|
||||
|
||||
func (s *StorageTestSuite[T, D]) TestReset() {
|
||||
func (s *StorageTestSuite[T, D, C]) TestReset() {
|
||||
s.setValue("key1", []byte("value1"))
|
||||
s.setValue("key2", []byte("value2"))
|
||||
|
||||
@@ -310,7 +355,7 @@ func (s *StorageTestSuite[T, D]) TestReset() {
|
||||
s.requireKeyNotExists("key2")
|
||||
}
|
||||
|
||||
func (s *StorageTestSuite[T, D]) TestResetWithContext() {
|
||||
func (s *StorageTestSuite[T, D, C]) TestResetWithContext() {
|
||||
s.setValue("key1", []byte("value1"))
|
||||
s.setValue("key2", []byte("value2"))
|
||||
|
||||
@@ -327,12 +372,12 @@ func (s *StorageTestSuite[T, D]) TestResetWithContext() {
|
||||
s.requireKeyHasValue("key2", []byte("value2"))
|
||||
}
|
||||
|
||||
func (s *StorageTestSuite[T, D]) TestClose() {
|
||||
func (s *StorageTestSuite[T, D, C]) TestClose() {
|
||||
err := s.store.Close()
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.closedStoreMu.Lock()
|
||||
defer s.closedStoreMu.Unlock()
|
||||
|
||||
s.closedStore = true
|
||||
}
|
||||
@@ -341,27 +386,27 @@ func (s *StorageTestSuite[T, D]) TestClose() {
|
||||
// Helpers
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
func (s *StorageTestSuite[T, D]) setValue(key string, value []byte) {
|
||||
func (s *StorageTestSuite[T, D, C]) setValue(key string, value []byte) {
|
||||
s.setValueWithTTL(key, value, 0)
|
||||
}
|
||||
|
||||
func (s *StorageTestSuite[T, D]) setValueWithTTL(key string, value []byte, ttl time.Duration) {
|
||||
func (s *StorageTestSuite[T, D, C]) setValueWithTTL(key string, value []byte, ttl time.Duration) {
|
||||
err := s.store.Set(key, value, ttl)
|
||||
s.Require().NoError(err)
|
||||
}
|
||||
|
||||
func (s *StorageTestSuite[T, D]) getValue(key string) []byte {
|
||||
func (s *StorageTestSuite[T, D, C]) getValue(key string) []byte {
|
||||
val, err := s.store.Get(key)
|
||||
s.Require().NoError(err)
|
||||
return val
|
||||
}
|
||||
|
||||
func (s *StorageTestSuite[T, D]) requireKeyHasValue(key string, expectedValue []byte) {
|
||||
func (s *StorageTestSuite[T, D, C]) requireKeyHasValue(key string, expectedValue []byte) {
|
||||
val := s.getValue(key)
|
||||
s.Require().Equal(expectedValue, val)
|
||||
}
|
||||
|
||||
func (s *StorageTestSuite[T, D]) requireKeyNotExists(key string) {
|
||||
func (s *StorageTestSuite[T, D, C]) requireKeyNotExists(key string) {
|
||||
val := s.getValue(key)
|
||||
s.Require().Nil(val)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user