Repository: uber-go/fx (go)

Issue:
OnStop_timeout test occasionally fails 
TestAppRunTimeout/OnStop timeout test occasionally fails on Windows leg because the Stopped event isn't emitted for some reason. Probably a flaky test and we should fix this. 
```
FIND: Parameter format not correct
go test -race -coverprofile=cover.out -coverpkg=./... ./...
--- FAIL: TestAppRunTimeout (0.00s)
    --- FAIL: TestAppRunTimeout/OnStop_timeout (0.00s)
        app_test.go:828: 
            	Error Trace:	app_test.go:828
            	Error:      	"[]" should have 1 item(s), but has 0
            	Test:       	TestAppRunTimeout/OnStop_timeout
            	Messages:   	expected a "Stopped" event
FAIL
```

Ref: GO-1001

Pull request:
Use mock clock, unflake tests
This PR resolves flakiness of some tests in Fx
by abstracting our use of time operations behind a Clock interface
and using a mock clock for the relevant tests.

This adds an internal/fxclock package that defines the Clock interface.
This interface is a compliant subset of ["benbjohnson/clock"][1].Clock.
This intentionally does not use benbjohnson/clock directly outside of tests
to keep it a test-only dependency.

[1]: https://github.com/benbjohnson/clock

The new *internal-only* `WithClock` option
accepts and plumbs an implementation of `fxclock.Clock`.
Note that this option is declared in app_internal_test.go.
It's not part of the public API,
but it's accessible to external tests in the top-level Fx directory.

Finally, this switches to using the Clock for time operations where relevant,
and updates tests that rely on sleep/time-based blocking to use a mock clock.
Now,

- `time.Now` is only present in fxclock.System.
- `time.Since` is only present in fxclock.System.
- `time.Sleep` is only present in fxclock.System.
- `context.WithTimeout` is present in fxclock.System,
  `fxtest.App.Require{Start,Stop}`, and `example_test.go`.
  The latter two because fxclock.Clock is not part of our public API.

Resolves #799
Ref: GO-1001




Patch:
diff --git a/app.go b/app.go
index 28fa3bf..5d7fe4a 100644
--- a/app.go
+++ b/app.go
@@ -35,6 +35,7 @@ import (
 
 	"go.uber.org/dig"
 	"go.uber.org/fx/fxevent"
+	"go.uber.org/fx/internal/fxclock"
 	"go.uber.org/fx/internal/fxlog"
 	"go.uber.org/fx/internal/fxreflect"
 	"go.uber.org/fx/internal/lifecycle"
@@ -364,6 +365,7 @@ var NopLogger = WithLogger(func() fxevent.Logger { return fxevent.NopLogger })
 // configurable deadline (again, 15 seconds by default).
 type App struct {
 	err       error
+	clock     fxclock.Clock
 	container *dig.Container
 	lifecycle *lifecycleWrapper
 	// Constructors and its dependencies.
@@ -515,6 +517,7 @@ func New(opts ...Option) *App {
 		// back to what was provided to fx.Logger if fx.WithLogger
 		// fails.
 		log:          logger,
+		clock:        fxclock.System,
 		startTimeout: DefaultTimeout,
 		stopTimeout:  DefaultTimeout,
 	}
@@ -533,7 +536,7 @@ func New(opts ...Option) *App {
 	// - appLogger ensures that the lifecycle always logs events to the
 	//   "current" logger associated with the fx.App.
 	app.lifecycle = &lifecycleWrapper{
-		lifecycle.New(appLogger{app}),
+		lifecycle.New(appLogger{app}, app.clock),
 	}
 
 	var (
@@ -671,7 +674,7 @@ func (app *App) Run() {
 }
 
 func (app *App) run(done <-chan os.Signal) (exitCode int) {
-	startCtx, cancel := context.WithTimeout(context.Background(), app.StartTimeout())
+	startCtx, cancel := app.clock.WithTimeout(context.Background(), app.StartTimeout())
 	defer cancel()
 
 	if err := app.Start(startCtx); err != nil {
@@ -681,7 +684,7 @@ func (app *App) run(done <-chan os.Signal) (exitCode int) {
 	sig := <-done
 	app.log.LogEvent(&fxevent.Stopping{Signal: sig})
 
-	stopCtx, cancel := context.WithTimeout(context.Background(), app.StopTimeout())
+	stopCtx, cancel := app.clock.WithTimeout(context.Background(), app.StopTimeout())
 	defer cancel()
 
 	if err := app.Stop(stopCtx); err != nil {
diff --git a/go.mod b/go.mod
index 02e0ec5..8e48cab 100644
--- a/go.mod
+++ b/go.mod
@@ -3,6 +3,7 @@ module go.uber.org/fx
 go 1.13
 
 require (
+	github.com/benbjohnson/clock v1.3.0
 	github.com/stretchr/testify v1.7.0
 	go.uber.org/dig v1.12.0
 	go.uber.org/goleak v1.1.11
diff --git a/go.sum b/go.sum
index ce720f6..b320780 100644
--- a/go.sum
+++ b/go.sum
@@ -1,5 +1,7 @@
 github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=
+github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
diff --git a/internal/fxclock/clock.go b/internal/fxclock/clock.go
new file mode 100644
index 0000000..bf1c705
--- /dev/null
+++ b/internal/fxclock/clock.go
@@ -0,0 +1,58 @@
+// Copyright (c) 2021 Uber Technologies, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+package fxclock
+
+import (
+	"context"
+	"time"
+)
+
+// Clock defines how Fx accesses time.
+// The interface is pretty minimal but it matches github.com/benbjohnson/clock.
+// We intentionally don't use that interface directly;
+// this keeps it a test dependency for us.
+type Clock interface {
+	Now() time.Time
+	Since(time.Time) time.Duration
+	Sleep(time.Duration)
+	WithTimeout(context.Context, time.Duration) (context.Context, context.CancelFunc)
+}
+
+// System is the default implementation of Clock based on real time.
+var System Clock = systemClock{}
+
+type systemClock struct{}
+
+func (systemClock) Now() time.Time {
+	return time.Now()
+}
+
+func (systemClock) Since(t time.Time) time.Duration {
+	return time.Since(t)
+}
+
+func (systemClock) Sleep(d time.Duration) {
+	time.Sleep(d)
+}
+
+func (systemClock) WithTimeout(ctx context.Context, d time.Duration) (context.Context, context.CancelFunc) {
+	return context.WithTimeout(ctx, d)
+}
diff --git a/internal/lifecycle/lifecycle.go b/internal/lifecycle/lifecycle.go
index fe36104..6916333 100644
--- a/internal/lifecycle/lifecycle.go
+++ b/internal/lifecycle/lifecycle.go
@@ -29,6 +29,7 @@ import (
 	"time"
 
 	"go.uber.org/fx/fxevent"
+	"go.uber.org/fx/internal/fxclock"
 	"go.uber.org/fx/internal/fxreflect"
 	"go.uber.org/multierr"
 )
@@ -44,6 +45,7 @@ type Hook struct {
 
 // Lifecycle coordinates application lifecycle hooks.
 type Lifecycle struct {
+	clock        fxclock.Clock
 	logger       fxevent.Logger
 	hooks        []Hook
 	numStarted   int
@@ -54,8 +56,8 @@ type Lifecycle struct {
 }
 
 // New constructs a new Lifecycle.
-func New(logger fxevent.Logger) *Lifecycle {
-	return &Lifecycle{logger: logger}
+func New(logger fxevent.Logger, clock fxclock.Clock) *Lifecycle {
+	return &Lifecycle{logger: logger, clock: clock}
 }
 
 // Append adds a Hook to the lifecycle.
@@ -114,9 +116,9 @@ func (l *Lifecycle) runStartHook(ctx context.Context, hook Hook) (runtime time.D
 		})
 	}()
 
-	begin := time.Now()
+	begin := l.clock.Now()
 	err = hook.OnStart(ctx)
-	return time.Since(begin), err
+	return l.clock.Since(begin), err
 }
 
 // Stop runs any OnStop hooks whose OnStart counterpart succeeded. OnStop
@@ -172,9 +174,9 @@ func (l *Lifecycle) runStopHook(ctx context.Context, hook Hook) (runtime time.Du
 		})
 	}()
 
-	begin := time.Now()
+	begin := l.clock.Now()
 	err = hook.OnStop(ctx)
-	return time.Since(begin), err
+	return l.clock.Since(begin), err
 }
 
 // StartHookRecords returns the info of OnStart hooks that successfully ran till the end,


Test patch:
diff --git a/app_internal_test.go b/app_internal_test.go
index b53c60b..197e4f7 100644
--- a/app_internal_test.go
+++ b/app_internal_test.go
@@ -29,6 +29,7 @@ import (
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 	"go.uber.org/fx/fxevent"
+	"go.uber.org/fx/internal/fxclock"
 	"go.uber.org/fx/internal/fxlog"
 	"go.uber.org/fx/internal/fxreflect"
 )
@@ -87,3 +88,20 @@ func (o withExitOption) String() string {
 func (o withExitOption) apply(app *App) {
 	app.osExit = o
 }
+
+// WithClock specifies how Fx accesses time operations.
+//
+// This is an internal option available only to tests defined in this package.
+func WithClock(clock fxclock.Clock) Option {
+	return withClockOption{clock}
+}
+
+type withClockOption struct{ clock fxclock.Clock }
+
+func (o withClockOption) apply(app *App) {
+	app.clock = o.clock
+}
+
+func (o withClockOption) String() string {
+	return fmt.Sprintf("WithClock(%v)", o.clock)
+}
diff --git a/app_test.go b/app_test.go
index 8b08633..70f5a37 100644
--- a/app_test.go
+++ b/app_test.go
@@ -34,6 +34,7 @@ import (
 	"testing"
 	"time"
 
+	"github.com/benbjohnson/clock"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 
@@ -752,26 +753,29 @@ func TestTimeoutOptions(t *testing.T) {
 func TestAppRunTimeout(t *testing.T) {
 	t.Parallel()
 
-	// This context is only valid as long as this test is running.
-	// It lets us have goroutines that block forever
-	// (as far as the test is concerned) without leaking them.
-	testCtx, cancel := context.WithCancel(context.Background())
-	t.Cleanup(cancel)
-
 	// Fails with an error immediately.
 	failure := func(context.Context) error {
 		return errors.New("great sadness")
 	}
 
-	// Blocks forever--or at least until this test finishes running.
-	blockForever := func(context.Context) error {
-		<-testCtx.Done()
-		return errors.New("user should not see this")
+	// Builds a hook that takes much longer than the application timeout.
+	takeVeryLong := func(clock *clock.Mock) func(ctx context.Context) error {
+		return func(ctx context.Context) error {
+			// We'll exceed the start and stop timeouts,
+			// and then some.
+			for i := 0; i < 3; i++ {
+				clock.Add(time.Second)
+			}
+
+			return errors.New("user should not see this")
+		}
 	}
 
 	tests := []struct {
-		desc  string
-		hooks []Hook
+		desc string
+
+		// buildHook builds and returns the hooks for this test case.
+		buildHooks func(*clock.Mock) []Hook
 
 		// Type of the fxevent we want.
 		// Does not reflect the exact value.
@@ -780,27 +784,33 @@ func TestAppRunTimeout(t *testing.T) {
 		{
 			// Timeout starting an application.
 			desc: "OnStart timeout",
-			hooks: []Hook{
-				{OnStart: blockForever},
+			buildHooks: func(clock *clock.Mock) []Hook {
+				return []Hook{
+					{OnStart: takeVeryLong(clock)},
+				}
 			},
 			wantEventType: &fxevent.Started{},
 		},
 		{
 			// Timeout during a rollback because start failed.
 			desc: "rollback timeout",
-			hooks: []Hook{
-				// The hooks are separate because
-				// OnStop will not be run if that hook failed.
-				{OnStop: blockForever},
-				{OnStart: failure},
+			buildHooks: func(clock *clock.Mock) []Hook {
+				return []Hook{
+					// The hooks are separate because
+					// OnStop will not be run if that hook failed.
+					{OnStop: takeVeryLong(clock)},
+					{OnStart: failure},
+				}
 			},
 			wantEventType: &fxevent.Started{},
 		},
 		{
 			// Timeout during a stop.
 			desc: "OnStop timeout",
-			hooks: []Hook{
-				{OnStop: blockForever},
+			buildHooks: func(clock *clock.Mock) []Hook {
+				return []Hook{
+					{OnStop: takeVeryLong(clock)},
+				}
 			},
 			wantEventType: &fxevent.Stopped{},
 		},
@@ -811,6 +821,8 @@ func TestAppRunTimeout(t *testing.T) {
 		t.Run(tt.desc, func(t *testing.T) {
 			t.Parallel()
 
+			mockClock := clock.NewMock()
+
 			var (
 				exitCode int
 				exited   bool
@@ -837,11 +849,13 @@ func TestAppRunTimeout(t *testing.T) {
 			}
 
 			app, spy := NewSpied(
-				StartTimeout(time.Millisecond),
-				StopTimeout(time.Millisecond),
+				StartTimeout(time.Second),
+				StopTimeout(time.Second),
 				WithExit(exit),
+				WithClock(mockClock),
 				Invoke(func(lc Lifecycle) {
-					for _, h := range tt.hooks {
+					hooks := tt.buildHooks(mockClock)
+					for _, h := range hooks {
 						lc.Append(h)
 					}
 				}),
@@ -875,12 +889,14 @@ func TestAppStart(t *testing.T) {
 	t.Run("Timeout", func(t *testing.T) {
 		t.Parallel()
 
+		mockClock := clock.NewMock()
+
 		type A struct{}
 		blocker := func(lc Lifecycle) *A {
 			lc.Append(
 				Hook{
 					OnStart: func(ctx context.Context) error {
-						<-ctx.Done()
+						mockClock.Add(5 * time.Second)
 						return ctx.Err()
 					},
 				},
@@ -894,11 +910,12 @@ func TestAppStart(t *testing.T) {
 		spy := new(fxlog.Spy)
 		app := New(
 			WithLogger(func() fxevent.Logger { return spy }),
+			WithClock(mockClock),
 			Provide(blocker),
 			Invoke(func(*A) {}),
 		)
 
-		ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond)
+		ctx, cancel := mockClock.WithTimeout(context.Background(), time.Second)
 
 		err := app.Start(ctx)
 		require.Error(t, err)
@@ -909,6 +926,8 @@ func TestAppStart(t *testing.T) {
 	t.Run("TimeoutWithFinishedHooks", func(t *testing.T) {
 		t.Parallel()
 
+		mockClock := clock.NewMock()
+
 		type A struct{}
 		type B struct{ A *A }
 		type C struct{ B *B }
@@ -916,6 +935,7 @@ func TestAppStart(t *testing.T) {
 			lc.Append(
 				Hook{
 					OnStart: func(context.Context) error {
+						mockClock.Add(100 * time.Millisecond)
 						return nil
 					},
 				},
@@ -926,7 +946,7 @@ func TestAppStart(t *testing.T) {
 			lc.Append(
 				Hook{
 					OnStart: func(context.Context) error {
-						time.Sleep(10 * time.Millisecond)
+						mockClock.Add(300 * time.Millisecond)
 						return nil
 					},
 				},
@@ -937,7 +957,7 @@ func TestAppStart(t *testing.T) {
 			lc.Append(
 				Hook{
 					OnStart: func(ctx context.Context) error {
-						<-ctx.Done()
+						mockClock.Add(5 * time.Second)
 						return ctx.Err()
 					},
 				},
@@ -952,11 +972,12 @@ func TestAppStart(t *testing.T) {
 		spy := new(fxlog.Spy)
 		app := New(
 			WithLogger(func() fxevent.Logger { return spy }),
+			WithClock(mockClock),
 			Provide(newA, newB, newC),
 			Invoke(func(*C) {}),
 		)
 
-		ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+		ctx, cancel := mockClock.WithTimeout(context.Background(), time.Second)
 		defer cancel()
 
 		err := app.Start(ctx)
@@ -1211,8 +1232,10 @@ func TestAppStop(t *testing.T) {
 	t.Run("Timeout", func(t *testing.T) {
 		t.Parallel()
 
+		mockClock := clock.NewMock()
+
 		block := func(ctx context.Context) error {
-			<-ctx.Done()
+			mockClock.Add(5 * time.Second)
 			return ctx.Err()
 		}
 		// NOTE: for tests that gets cancelled/times out during lifecycle methods, it's possible
@@ -1222,12 +1245,13 @@ func TestAppStop(t *testing.T) {
 		spy := new(fxlog.Spy)
 		app := New(Invoke(func(l Lifecycle) { l.Append(Hook{OnStop: block}) }),
 			WithLogger(func() fxevent.Logger { return spy }),
+			WithClock(mockClock),
 		)
 
 		err := app.Start(context.Background())
 		require.Nil(t, err)
 
-		ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond)
+		ctx, cancel := mockClock.WithTimeout(context.Background(), time.Second)
 		defer cancel()
 
 		err = app.Stop(ctx)
diff --git a/fxtest/lifecycle.go b/fxtest/lifecycle.go
index 35ec2b9..5de1f4a 100644
--- a/fxtest/lifecycle.go
+++ b/fxtest/lifecycle.go
@@ -27,6 +27,7 @@ import (
 	"os"
 
 	"go.uber.org/fx"
+	"go.uber.org/fx/internal/fxclock"
 	"go.uber.org/fx/internal/fxlog"
 	"go.uber.org/fx/internal/lifecycle"
 	"go.uber.org/fx/internal/testutil"
@@ -85,7 +86,7 @@ func NewLifecycle(t TB) *Lifecycle {
 		t = &panicT{W: os.Stderr}
 	}
 	return &Lifecycle{
-		lc: lifecycle.New(fxlog.DefaultLogger(w)),
+		lc: lifecycle.New(fxlog.DefaultLogger(w), fxclock.System),
 		t:  t,
 	}
 }
diff --git a/internal/fxclock/clock_test.go b/internal/fxclock/clock_test.go
new file mode 100644
index 0000000..560867e
--- /dev/null
+++ b/internal/fxclock/clock_test.go
@@ -0,0 +1,65 @@
+// Copyright (c) 2021 Uber Technologies, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+package fxclock
+
+import (
+	"context"
+	"testing"
+	"time"
+
+	"github.com/benbjohnson/clock"
+	"github.com/stretchr/testify/assert"
+)
+
+var _ Clock = clock.Clock(nil)
+
+// Just a basic sanity check that everything is in order.
+func TestSystemClock(t *testing.T) {
+	t.Parallel()
+
+	clock := System
+
+	t.Run("Now and Since", func(t *testing.T) {
+		t.Parallel()
+
+		before := clock.Now()
+		assert.GreaterOrEqual(t, clock.Since(before), time.Duration(0))
+	})
+
+	t.Run("Sleep", func(t *testing.T) {
+		t.Parallel()
+
+		assert.NotPanics(t, func() {
+			clock.Sleep(time.Millisecond)
+		})
+	})
+
+	t.Run("WithTimeout", func(t *testing.T) {
+		t.Parallel()
+
+		ctx := context.Background()
+		ctx, cancel := clock.WithTimeout(ctx, time.Second)
+		defer cancel()
+
+		_, ok := ctx.Deadline()
+		assert.True(t, ok, "must have deadline")
+	})
+}
diff --git a/internal/lifecycle/lifecycle_test.go b/internal/lifecycle/lifecycle_test.go
index 778bdf9..f325de5 100644
--- a/internal/lifecycle/lifecycle_test.go
+++ b/internal/lifecycle/lifecycle_test.go
@@ -32,6 +32,7 @@ import (
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 	"go.uber.org/fx/fxevent"
+	"go.uber.org/fx/internal/fxclock"
 	"go.uber.org/fx/internal/fxlog"
 	"go.uber.org/fx/internal/fxreflect"
 	"go.uber.org/fx/internal/testutil"
@@ -49,7 +50,7 @@ func TestLifecycleStart(t *testing.T) {
 	t.Run("ExecutesInOrder", func(t *testing.T) {
 		t.Parallel()
 
-		l := New(testLogger(t))
+		l := New(testLogger(t), fxclock.System)
 		count := 0
 
 		l.Append(Hook{
@@ -73,7 +74,7 @@ func TestLifecycleStart(t *testing.T) {
 	t.Run("ErrHaltsChainAndRollsBack", func(t *testing.T) {
 		t.Parallel()
 
-		l := New(testLogger(t))
+		l := New(testLogger(t), fxclock.System)
 		err := errors.New("a starter error")
 		starterCount := 0
 		stopperCount := 0
@@ -126,7 +127,7 @@ func TestLifecycleStop(t *testing.T) {
 	t.Run("DoesNothingWithoutHooks", func(t *testing.T) {
 		t.Parallel()
 
-		l := &Lifecycle{logger: testLogger(t)}
+		l := New(testLogger(t), fxclock.System)
 		assert.Nil(t, l.Stop(context.Background()), "no lifecycle hooks should have resulted in stop returning nil")
 	})
 
@@ -139,7 +140,7 @@ func TestLifecycleStop(t *testing.T) {
 				return nil
 			},
 		}
-		l := &Lifecycle{logger: testLogger(t)}
+		l := New(testLogger(t), fxclock.System)
 		l.Append(hook)
 		l.Stop(context.Background())
 	})
@@ -147,7 +148,7 @@ func TestLifecycleStop(t *testing.T) {
 	t.Run("ExecutesInReverseOrder", func(t *testing.T) {
 		t.Parallel()
 
-		l := &Lifecycle{logger: testLogger(t)}
+		l := New(testLogger(t), fxclock.System)
 		count := 2
 
 		l.Append(Hook{
@@ -173,7 +174,7 @@ func TestLifecycleStop(t *testing.T) {
 	t.Run("ErrDoesntHaltChain", func(t *testing.T) {
 		t.Parallel()
 
-		l := New(testLogger(t))
+		l := New(testLogger(t), fxclock.System)
 		count := 0
 
 		l.Append(Hook{
@@ -197,7 +198,7 @@ func TestLifecycleStop(t *testing.T) {
 	t.Run("GathersAllErrs", func(t *testing.T) {
 		t.Parallel()
 
-		l := New(testLogger(t))
+		l := New(testLogger(t), fxclock.System)
 
 		err := errors.New("some stop error")
 		err2 := errors.New("some other stop error")
@@ -219,7 +220,7 @@ func TestLifecycleStop(t *testing.T) {
 	t.Run("AllowEmptyHooks", func(t *testing.T) {
 		t.Parallel()
 
-		l := New(testLogger(t))
+		l := New(testLogger(t), fxclock.System)
 		l.Append(Hook{})
 		l.Append(Hook{})
 
@@ -230,7 +231,7 @@ func TestLifecycleStop(t *testing.T) {
 	t.Run("DoesNothingIfStartFailed", func(t *testing.T) {
 		t.Parallel()
 
-		l := New(testLogger(t))
+		l := New(testLogger(t), fxclock.System)
 		err := errors.New("some start error")
 
 		l.Append(Hook{