full unit testing and fixes

- mapToString is ordered according to setup
- setupSingle & teardownSingle no longer return "ok" states
- teardown functions support errors
- setup and teardown functions receive module
This commit is contained in:
Elijah Duffy
2025-06-03 14:49:00 -07:00
parent 5378c53537
commit ed8137647d
4 changed files with 695 additions and 65 deletions

View File

@@ -1,16 +1,13 @@
package app package app
import ( import (
"errors"
"fmt" "fmt"
"log/slog" "log/slog"
"sort"
"strings"
) )
// setupFn is a function that runs setup logic for a module.
type setupFn func() error
// teardownFn is a function that runs teardown logic for a module.
type teardownFn func()
// LifecycleOpts contains user-exposed options when defining a lifecycle. // LifecycleOpts contains user-exposed options when defining a lifecycle.
type LifecycleOpts struct { type LifecycleOpts struct {
// DisableAutoload disables dependency autoloading (enabled by default). // DisableAutoload disables dependency autoloading (enabled by default).
@@ -25,9 +22,12 @@ type Lifecycle struct {
logger *slog.Logger logger *slog.Logger
modules []*Module modules []*Module
setupTracker map[string]bool
teardownTracker map[string]bool
opts LifecycleOpts opts LifecycleOpts
setupCount int
setupTracker map[string]int
teardownCount int
teardownTracker map[string]int
} }
// NewLifecycle creates a new Lifecycle instance with a default logger and the // NewLifecycle creates a new Lifecycle instance with a default logger and the
@@ -56,8 +56,8 @@ func NewLifecycleL(defaultLogger *slog.Logger, modules ...*Module) *Lifecycle {
return &Lifecycle{ return &Lifecycle{
logger: defaultLogger, logger: defaultLogger,
modules: modules, modules: modules,
setupTracker: make(map[string]bool), setupTracker: make(map[string]int),
teardownTracker: make(map[string]bool), teardownTracker: make(map[string]int),
} }
} }
@@ -80,36 +80,37 @@ func (app *Lifecycle) Logger() *slog.Logger {
// if dependencies are not satisfied. Lifecycle.Teardown should always be run at the end // if dependencies are not satisfied. Lifecycle.Teardown should always be run at the end
// of the application lifecycle to ensure all resources are cleaned up properly. // of the application lifecycle to ensure all resources are cleaned up properly.
func (app *Lifecycle) Setup() error { func (app *Lifecycle) Setup() error {
setupCount := 0
for _, mod := range app.modules { for _, mod := range app.modules {
if ok, err := app.setupSingle(mod, app.logger); err != nil { if err := app.setupSingle(mod, app.logger); err != nil {
return err return err
} else if ok {
setupCount++
} }
} }
app.logger.Info("Lifecycle modules initialized, backend setup complete", app.logger.Info("Lifecycle modules initialized, backend setup complete",
"count", setupCount, "modules", mapToString(app.setupTracker)) "modules", mapToString(app.setupTracker))
return nil return nil
} }
// Teardown runs all module teardown functions in reverse order of setup. // Teardown runs all module teardown functions in reverse order of setup.
// Teardown should always be run at the end of the application lifecycle // Teardown should always be run at the end of the application lifecycle
// to ensure all resources are cleaned up properly. // to ensure all resources are cleaned up properly. All module tear down
// errors are returned as a single error (non-blocking).
func (app *Lifecycle) Teardown() error { func (app *Lifecycle) Teardown() error {
teardownCount := 0 var err error
for i := len(app.modules) - 1; i >= 0; i-- { for i := len(app.modules) - 1; i >= 0; i-- {
if ok, err := app.singleTeardown(app.modules[i]); err != nil { if singleErr := app.teardownSingle(app.modules[i]); singleErr != nil {
return err err = errors.Join(err, singleErr)
} else if ok {
teardownCount++
} }
} }
if err != nil {
app.logger.Error("Error tearing down modules", "failures", app.setupCount-app.teardownCount, "error", err)
return err
}
app.logger.Info("All modules torn down, backend teardown complete", app.logger.Info("All modules torn down, backend teardown complete",
"count", teardownCount, "modules", mapToString(app.teardownTracker)) "modules", mapToString(app.teardownTracker))
return nil return nil
} }
@@ -127,6 +128,14 @@ func (app *Lifecycle) Require(modules ...*Module) error {
// This variation is useful when you need to set up modules with a non- // This variation is useful when you need to set up modules with a non-
// default logger. // default logger.
func (app *Lifecycle) RequireL(logger *slog.Logger, modules ...*Module) error { func (app *Lifecycle) RequireL(logger *slog.Logger, modules ...*Module) error {
if logger == nil {
logger = app.logger
}
if len(modules) == 0 {
return fmt.Errorf("no modules to require")
}
for _, mod := range modules { for _, mod := range modules {
if mod == nil { if mod == nil {
return fmt.Errorf("module is nil") return fmt.Errorf("module is nil")
@@ -135,6 +144,8 @@ func (app *Lifecycle) RequireL(logger *slog.Logger, modules ...*Module) error {
// Check if the module is already set up // Check if the module is already set up
if _, ok := app.setupTracker[mod.name]; ok { if _, ok := app.setupTracker[mod.name]; ok {
app.logger.Warn("module already set up, ignoring", "module", mod.name) app.logger.Warn("module already set up, ignoring", "module", mod.name)
mod.loaded = true
mod.logger = logger
continue continue
} }
@@ -142,7 +153,7 @@ func (app *Lifecycle) RequireL(logger *slog.Logger, modules ...*Module) error {
app.modules = append(app.modules, mod) app.modules = append(app.modules, mod)
// Run the setup function for the module // Run the setup function for the module
if _, err := app.setupSingle(mod, logger); err != nil { if err := app.setupSingle(mod, logger); err != nil {
return fmt.Errorf("error setting up required module '%s': %w", mod.name, err) return fmt.Errorf("error setting up required module '%s': %w", mod.name, err)
} }
} }
@@ -152,10 +163,11 @@ func (app *Lifecycle) RequireL(logger *slog.Logger, modules ...*Module) error {
return nil return nil
} }
// setupSingle is a helper function to set up a single module. // setupSingle is a helper function to set up a single module. Returns an error
func (app *Lifecycle) setupSingle(mod *Module, logger *slog.Logger) (bool, error) { // if the module cannot be set up or if dependencies are not satisfied.
func (app *Lifecycle) setupSingle(mod *Module, logger *slog.Logger) error {
if mod == nil { if mod == nil {
return false, fmt.Errorf("module is nil") return fmt.Errorf("module is nil")
} }
// Set the logger for the module // Set the logger for the module
@@ -165,59 +177,59 @@ func (app *Lifecycle) setupSingle(mod *Module, logger *slog.Logger) (bool, error
for _, dep := range mod.depends { for _, dep := range mod.depends {
if _, ok := app.setupTracker[dep]; !ok { if _, ok := app.setupTracker[dep]; !ok {
if app.opts.DisableAutoload { if app.opts.DisableAutoload {
return false, fmt.Errorf("dependency '%s' not satisfied for '%s'", dep, mod.name) return fmt.Errorf("dependency '%s' not satisfied for '%s'", dep, mod.name)
} else { } else {
// Attempt to set up the dependency // Attempt to set up the dependency
mod, err := app.getModuleByName(dep) depmod, err := app.getModuleByName(dep)
if err != nil { if err != nil {
return false, err return fmt.Errorf("error getting dependency '%s' for '%s': %w", dep, mod.name, err)
} }
if _, err := app.setupSingle(mod, logger); err != nil { if err := app.setupSingle(depmod, logger); err != nil {
return false, fmt.Errorf("error setting up dependency '%s' for '%s': %w", dep, mod.name, err) return fmt.Errorf("error setting up dependency '%s' for '%s': %w", dep, mod.name, err)
} }
} }
} }
} }
if mod.setup != nil { if mod.setup != nil {
// Run the setup function for the module // Run the setup function for the module
if err := mod.setup(); err != nil { if err := mod.setup(mod); err != nil {
return false, fmt.Errorf("error initializing '%s': %w", mod.name, err) return fmt.Errorf("error initializing '%s': %w", mod.name, err)
}
} }
// Mark this module as setup // Mark this module as setup
app.setupTracker[mod.name] = true app.setupTracker[mod.name] = app.setupCount
app.setupCount++
mod.loaded = true mod.loaded = true
return true, nil return nil
} }
return false, nil // teardownSingle is a helper function to tear down a single module. Returns
} // an error if anything goes wrong.
func (app *Lifecycle) teardownSingle(mod *Module) error {
// singleTeardown is a helper function to tear down a single module.
func (app *Lifecycle) singleTeardown(mod *Module) (bool, error) {
if mod == nil { if mod == nil {
return false, fmt.Errorf("module is nil") return fmt.Errorf("module is nil")
} }
// Check if the module was set up // Check if the module was set up
if _, ok := app.setupTracker[mod.name]; !ok { if _, ok := app.setupTracker[mod.name]; !ok {
return false, nil return fmt.Errorf("module '%s' is not set up, cannot tear down", mod.name)
} }
// Run the teardown function for the module // Run the teardown function for the module
if mod.teardown != nil { if mod.teardown != nil {
mod.teardown() if err := mod.teardown(mod); err != nil {
return fmt.Errorf("error tearing down '%s': %w", mod.name, err)
// Mark this module as torn down }
app.teardownTracker[mod.name] = true
mod.loaded = false
return true, nil
} }
return false, nil // Mark this module as torn down
app.teardownTracker[mod.name] = app.teardownCount
app.teardownCount++
mod.loaded = false
return nil
} }
// getModuleByName retrieves a module by its name from the lifecycle. // getModuleByName retrieves a module by its name from the lifecycle.
@@ -227,21 +239,30 @@ func (app *Lifecycle) getModuleByName(name string) (*Module, error) {
return mod, nil return mod, nil
} }
} }
return nil, fmt.Errorf("module '%s' not found", name) return nil, ErrModuleNotFound
} }
// mapToString converts a map to an opinionated string representation. // mapToString converts a map to an ordered, opinionated string representation.
func mapToString(m map[string]bool) string { func mapToString(m map[string]int) string {
if len(m) == 0 { if len(m) == 0 {
return "[]" return "[]"
} }
result := "[" // Sort the map by value
values := make([]int, 0, len(m))
reverseMap := make(map[int]string, len(m))
for k, v := range m { for k, v := range m {
if v { values = append(values, v)
result += fmt.Sprintf("'%s' ", k) reverseMap[v] = k
}
sort.Ints(values)
result := make([]string, 0, len(m))
for _, v := range values {
if name, ok := reverseMap[v]; ok {
result = append(result, fmt.Sprintf("'%s'", name))
} }
} }
result = result[:len(result)-1] + "]"
return result return "[" + strings.Join(result, " ") + "]"
} }

541
lifecycle_test.go Normal file
View File

@@ -0,0 +1,541 @@
package app
import (
"errors"
"log/slog"
"testing"
"github.com/stretchr/testify/assert"
)
func TestNewLifecycle(t *testing.T) {
assert := assert.New(t)
lc := NewLifecycle()
assert.NotNil(lc, "expected Lifecycle instance to be created")
assert.Equal(slog.Default(), lc.logger, "expected default logger to be set")
}
func TestNewLifecycleL(t *testing.T) {
assert := assert.New(t)
// Create a blank Lifecycle instance with a custom logger
logger := slog.Default()
lc := NewLifecycleL(logger)
assert.NotNil(lc)
assert.Equal(logger, lc.logger, "expected logger to be set")
assert.Len(lc.modules, 0, "expected modules to be empty")
assert.Len(lc.setupTracker, 0, "expected setup tracker to be empty")
assert.Len(lc.teardownTracker, 0, "expected teardown tracker to be empty")
// Test with modules
mod1 := NewModule("module1", ModuleOpts{})
mod2 := NewModule("module2", ModuleOpts{})
lcWithModules := NewLifecycleL(logger, mod1, mod2)
assert.NotNil(lcWithModules, "expected Lifecycle with modules to be created")
assert.Contains(lcWithModules.modules, mod1, "expected module1 to be included")
assert.Contains(lcWithModules.modules, mod2, "expected module2 to be included")
assert.Len(lcWithModules.modules, 2, "expected two modules to be present")
// Test for duplicate module names
assert.PanicsWithValue("duplicate module name: module1", func() {
NewLifecycleL(logger, mod1, mod1)
}, "expected panic on duplicate module names")
}
func TestLifecycle_WithOpts(t *testing.T) {
assert := assert.New(t)
lc := NewLifecycle()
opts := LifecycleOpts{
DisableAutoload: true,
}
lc.WithOpts(opts)
assert.Equal(opts, lc.opts, "expected Lifecycle to have the provided options")
}
func TestLifecycle_Logger(t *testing.T) {
assert := assert.New(t)
lc := NewLifecycle()
assert.Equal(slog.Default(), lc.Logger(), "expected Logger to return a logger")
// Test panic when logger is not initialized
lc.logger = nil
assert.PanicsWithValue("lifecycle logger is not initialized", func() {
lc.Logger()
}, "expected Logger to panic when logger is not set")
}
func TestLifecycle_Setup(t *testing.T) {
cases := []struct {
name string
modules []*Module
expectedErr string
}{
{
name: "empty lifecycle",
modules: nil,
},
{
name: "single module with setup",
modules: []*Module{
NewModule("module1", ModuleOpts{
Setup: func(m *Module) error { return nil },
}),
},
},
{
name: "multiple modules",
modules: []*Module{
NewModule("module1", ModuleOpts{}),
NewModule("module2", ModuleOpts{
Setup: func(m *Module) error { return nil },
}),
},
},
{
name: "module with setup error",
modules: []*Module{
NewModule("module1", ModuleOpts{
Setup: func(m *Module) error { return errors.New("setup failed") },
}),
},
expectedErr: "setup failed",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
assert := assert.New(t)
lc := NewLifecycle(tc.modules...)
err := lc.Setup()
if tc.expectedErr == "" {
assert.NoError(err, "expected Setup to succeed")
for _, mod := range tc.modules {
if mod.setup != nil {
_, ok := lc.setupTracker[mod.name]
assert.True(ok, "expected module to be marked as set up")
}
}
} else {
assert.Error(err, "expected Setup to fail")
assert.Contains(err.Error(), tc.expectedErr, "expected error message to match")
}
})
}
}
func TestLifecycle_Teardown(t *testing.T) {
cases := []struct {
name string
modules []*Module
expectedErr []string
}{
{
name: "empty lifecycle",
modules: nil,
},
{
name: "single module with teardown",
modules: []*Module{
NewModule("module1", ModuleOpts{
Teardown: func(m *Module) error { return nil },
}),
},
},
{
name: "multiple modules",
modules: []*Module{
NewModule("module1", ModuleOpts{}),
NewModule("module2", ModuleOpts{
Teardown: func(m *Module) error { return nil },
}),
},
},
{
name: "module with teardown error",
modules: []*Module{
NewModule("module1", ModuleOpts{
Teardown: func(m *Module) error { return errors.New("teardown failed") },
}),
},
expectedErr: []string{"teardown failed"},
},
{
name: "multiple modules with teardown errors",
modules: []*Module{
NewModule("module1", ModuleOpts{
Teardown: func(m *Module) error { return errors.New("module1 teardown failed") },
}),
NewModule("module2", ModuleOpts{
Teardown: func(m *Module) error { return errors.New("module2 teardown failed") },
}),
},
expectedErr: []string{"module1 teardown failed", "module2 teardown failed"},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
assert := assert.New(t)
lc := NewLifecycle(tc.modules...)
// Fake setup for all modules
for _, mod := range tc.modules {
lc.setupTracker[mod.name] = 0 // Mark as set up
}
err := lc.Teardown()
if len(tc.expectedErr) == 0 {
assert.NoError(err, "expected Teardown to succeed")
for _, mod := range tc.modules {
if mod.teardown != nil {
_, ok := lc.teardownTracker[mod.name]
assert.True(ok, "expected module to be marked as torn down")
}
}
} else {
assert.Error(err, "expected Teardown to fail")
for _, expected := range tc.expectedErr {
assert.Contains(err.Error(), expected, "expected error message to match")
}
}
})
}
}
func TestLifecycle_Require(t *testing.T) {
lc := NewLifecycle()
mod := NewModule("module1", ModuleOpts{
Setup: func(m *Module) error { return nil },
})
lc.Require(mod)
assert.Equal(t, lc.logger, mod.logger, "expected module logger to match lifecycle logger")
}
func TestLifecycle_RequireL(t *testing.T) {
cases := []struct {
name string
logger *slog.Logger
modules []*Module
existingModules []*Module
expectedErr string
}{
{
name: "no modules",
expectedErr: "no modules to require",
},
{
name: "nil module",
modules: []*Module{nil},
expectedErr: "module is nil",
},
{
name: "basic module",
modules: []*Module{
NewModule("testModule", ModuleOpts{
Setup: func(m *Module) error {
if m.name != "testModule" {
return errors.New("module name mismatch")
}
return nil
},
}),
},
},
{
name: "several modules",
modules: []*Module{
NewModule("module1", ModuleOpts{
Setup: func(m *Module) error { return nil },
}),
NewModule("module2", ModuleOpts{
Setup: func(m *Module) error { return nil },
}),
},
},
{
name: "module with setup error",
modules: []*Module{
NewModule("errorModule", ModuleOpts{
Setup: func(m *Module) error { return errors.New("setup failed") },
}),
},
expectedErr: "setup failed",
},
{
name: "module with dependency",
modules: []*Module{
NewModule("dependentModule", ModuleOpts{
Setup: func(m *Module) error { return nil },
Depends: []string{"dependencyModule"},
}),
},
existingModules: []*Module{
NewModule("dependencyModule", ModuleOpts{
Setup: func(m *Module) error { return nil },
}),
},
},
{
name: "duplicate module names",
modules: []*Module{
NewModule("duplicateModule", ModuleOpts{}),
NewModule("duplicateModule", ModuleOpts{}),
},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
assert := assert.New(t)
lc := NewLifecycle(tc.existingModules...)
l := tc.logger
if l == nil {
l = lc.logger
}
err := lc.RequireL(tc.logger, tc.modules...)
if tc.expectedErr == "" {
assert.NoError(err, "expected RequireL to succeed")
for _, mod := range tc.modules {
if mod != nil {
_, ok := lc.setupTracker[mod.name]
assert.True(ok, "expected module to be marked as set up")
assert.True(mod.loaded, "expected module to be loaded")
assert.Equal(l, mod.logger, "expected module logger to match")
}
}
} else {
assert.Error(err, "expected RequireL to fail")
assert.Contains(err.Error(), tc.expectedErr, "expected error message to match")
}
})
}
}
func TestLifecycle_setupSingle(t *testing.T) {
cases := []struct {
name string
lc *Lifecycle
targetModule *Module
modules []*Module
expectedErr string
logger *slog.Logger
}{
{
name: "nil module",
targetModule: nil,
expectedErr: "module is nil",
},
{
name: "basic module setup",
targetModule: NewModule("testModule", ModuleOpts{
Setup: func(m *Module) error {
if m.name != "testModule" {
return errors.New("module name mismatch")
}
return nil
},
}),
},
{
name: "module with dependencies",
targetModule: NewModule("dependentModule", ModuleOpts{
Setup: func(m *Module) error { return nil },
Depends: []string{"dependencyModule"},
}),
modules: []*Module{
NewModule("dependencyModule", ModuleOpts{
Setup: func(m *Module) error { return nil },
}),
},
},
{
name: "module with missing dependency",
targetModule: NewModule("dependentModule", ModuleOpts{
Setup: func(m *Module) error { return nil },
Depends: []string{"missingDependency"},
}),
expectedErr: "error getting dependency",
},
{
name: "module setup error",
targetModule: NewModule("errorModule", ModuleOpts{
Setup: func(m *Module) error { return errors.New("setup failed") },
}),
expectedErr: "setup failed",
},
{
name: "module dependency setup error",
targetModule: NewModule("moduleWithoutError", ModuleOpts{
Setup: func(m *Module) error { return nil },
Depends: []string{"errorModule"},
}),
modules: []*Module{
NewModule("errorModule", ModuleOpts{
Setup: func(m *Module) error { return errors.New("dependency setup failed") },
}),
},
expectedErr: "error initializing 'errorModule'",
},
{
name: "dependency without autoload",
lc: NewLifecycle().WithOpts(LifecycleOpts{
DisableAutoload: true,
}),
targetModule: NewModule("dependentModule", ModuleOpts{
Setup: func(m *Module) error { return nil },
Depends: []string{"dependencyModule"},
}),
expectedErr: "not satisfied",
},
{
name: "module without setup function",
targetModule: NewModule("noSetupModule", ModuleOpts{}),
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
assert := assert.New(t)
l := tc.logger
if l == nil {
l = slog.Default()
}
lc := tc.lc
if lc == nil {
lc = NewLifecycle(tc.modules...)
}
err := lc.setupSingle(tc.targetModule, l)
if tc.targetModule != nil {
assert.Equal(l, tc.targetModule.logger, "expected module logger to match")
}
if tc.expectedErr == "" {
assert.NoError(err, "expected no error from setupSingle")
} else {
assert.Contains(err.Error(), tc.expectedErr, "expected error message to match")
}
})
}
}
func TestLifecycle_teardownSingle(t *testing.T) {
cases := []struct {
name string
targetModule *Module
modules []string
expectedErr string
}{
{
name: "nil module",
expectedErr: "module is nil",
},
{
name: "basic module teardown",
targetModule: NewModule("testModule", ModuleOpts{
Teardown: func(m *Module) error {
if m.name != "testModule" {
return errors.New("module name mismatch")
}
return nil
},
}),
modules: []string{"testModule"},
},
{
name: "module not set up",
targetModule: NewModule("notSetUpModule", ModuleOpts{
Teardown: func(m *Module) error { return nil },
}),
expectedErr: "not set up, cannot tear down",
},
{
name: "module without teardown function",
targetModule: NewModule("noTeardownModule", ModuleOpts{}),
modules: []string{"noTeardownModule"},
},
{
name: "module with teardown error",
targetModule: NewModule("errorModule", ModuleOpts{
Teardown: func(m *Module) error { return errors.New("teardown failed") },
}),
modules: []string{"errorModule"},
expectedErr: "error tearing down",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
assert := assert.New(t)
lc := NewLifecycle()
// Fake setup for all modules
var setupCount int
for _, mod := range tc.modules {
lc.setupTracker[mod] = setupCount
setupCount++
}
err := lc.teardownSingle(tc.targetModule)
if tc.expectedErr == "" {
assert.NoError(err, "expected no error from teardownSingle")
} else {
assert.Contains(err.Error(), tc.expectedErr, "expected error message to match")
}
})
}
}
func Test_mapToString(t *testing.T) {
cases := []struct {
name string
input map[string]int
expected string
}{
{
name: "empty map",
input: map[string]int{},
expected: "[]",
},
{
name: "single entry",
input: map[string]int{"key1": 0},
expected: "['key1']",
},
{
name: "multiple entries",
input: map[string]int{"key1": 0, "key2": 3, "key3": 2},
expected: "['key1' 'key3' 'key2']",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
assert := assert.New(t)
result := mapToString(tc.input)
assert.Equal(tc.expected, result, "expected mapToString to return correct string representation")
})
}
}

View File

@@ -1,17 +1,24 @@
package app package app
import ( import (
"errors"
"fmt" "fmt"
"log/slog" "log/slog"
) )
// ErrModuleNotFound is returned when a module is not found in the lifecycle.
var ErrModuleNotFound = errors.New("module not found")
// moduleFn is a function type for module setup and teardown functions.
type moduleFn func(*Module) error
// Module represents a sub-system of the application, with its setup and // Module represents a sub-system of the application, with its setup and
// teardown functions and dependencies. // teardown functions and dependencies.
type Module struct { type Module struct {
logger *slog.Logger logger *slog.Logger
name string name string
setup setupFn setup moduleFn
teardown teardownFn teardown moduleFn
depends []string depends []string
loaded bool // loaded indicates if the module has been set up loaded bool // loaded indicates if the module has been set up
} }
@@ -19,9 +26,9 @@ type Module struct {
// ModuleOpts contains user-exposed options when defining a module. // ModuleOpts contains user-exposed options when defining a module.
type ModuleOpts struct { type ModuleOpts struct {
// Setup is the setup function for the module. // Setup is the setup function for the module.
Setup setupFn Setup moduleFn
// Teardown is the teardown function for the module. // Teardown is the teardown function for the module.
Teardown teardownFn Teardown moduleFn
// Depends is a list of modules that this module depends on. Each entry must // Depends is a list of modules that this module depends on. Each entry must
// be the exact name of a module registered in the lifecycle. // be the exact name of a module registered in the lifecycle.
Depends []string Depends []string

61
module_test.go Normal file
View File

@@ -0,0 +1,61 @@
package app
import (
"log/slog"
"testing"
"github.com/stretchr/testify/assert"
)
func TestNewModule(t *testing.T) {
assert := assert.New(t)
// Test creating a module with no options
mod := NewModule("testModule", ModuleOpts{})
assert.NotNil(mod, "expected module to be created")
assert.Equal("testModule", mod.name, "expected module name to match")
assert.Nil(mod.setup, "expected setup function to be nil")
assert.Nil(mod.teardown, "expected teardown function to be nil")
assert.Empty(mod.depends, "expected dependencies to be empty")
assert.False(mod.loaded, "expected module to not be loaded")
// Test creating a module with setup and teardown functions and dependencies
setupFn := func(m *Module) error { return nil }
teardownFn := func(m *Module) error { return nil }
modWithOpts := NewModule("testModuleWithOpts", ModuleOpts{
Setup: setupFn,
Teardown: teardownFn,
Depends: []string{"dependency1", "dependency2"},
})
assert.NotNil(modWithOpts, "expected module with options to be created")
assert.NotNil(modWithOpts.setup, "expected setup function to be set")
assert.NotNil(modWithOpts.teardown, "expected teardown function to be set")
assert.Equal([]string{"dependency1", "dependency2"}, modWithOpts.depends, "expected dependencies to match")
}
func TestModule(t *testing.T) {
assert := assert.New(t)
// Create a module and set its logger
mod := NewModule("testModule", ModuleOpts{})
assert.Panics(func() { mod.Logger() }, "expected Logger to panic when logger is not set")
logger := slog.Default()
mod.logger = logger
// Test Logger method
assert.Equal(logger, mod.Logger(), "expected Logger to return the correct logger")
// Test Name method
assert.Equal("testModule", mod.Name(), "expected Name to return the module's name")
// Test Loaded method
assert.False(mod.Loaded(), "expected Loaded to return false initially")
// Test RequireLoaded method
assert.Panics(func() { mod.RequireLoaded() }, "expected RequireLoaded to panic when module is not loaded")
// Test setting the module as loaded
mod.loaded = true
assert.NotPanics(func() { mod.RequireLoaded() }, "expected RequireLoaded to not panic when module is loaded")
}