clock

package
v0.178.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 8, 2025 License: Apache-2.0 Imports: 2 Imported by: 9

README

Clock and Timecop

The clock package is designed to make testing time-dependent code more straightforward and reliable. Acting as a drop-in replacement for the standard time package, it introduces advanced features such as "time travel" and "time scaling", making it easier to simulate and control time within your tests.

It’s important to note that these enhanced features are available only during testing. In a production environment, clock is a direct alias to the stdlib time package, without any additional capabilities. So, it’s worth noting that clock is intentionally designed not to allow time manipulation in production. If your use case involves altering time in a live system, you may need to explore alternative solutions better suited to that need.

For some concurent programming related testing scenaris, it is also worth to look into using testing/synctest.

INSTALL

go get go.llib.dev/testcase/clock

FEATURES

  • Drop in replacement for standard time package
  • Freeze time to a specific point.
  • Travel back to a specific time, but allow time to continue moving forward.
  • Scale time by a given scaling factor will cause the time to move at an accelerated pace.
  • No dependencies other than the stdlib
  • Continous and nested calls with timecop.Travel are supported
  • Works with any regular Go projects

Freezing time

Using the timecop package, you can freeze time in clock at different levels during a time travelling.

The first level, timecop.Freeze, stops the timeline from moving forward but doesn't halt tickers or functions that depend on time. This is useful for testing components that depends on background goroutines that has time-based scheduling, allowing events to fire while ensuring you can assert time values in entity fields. Think of it like freezing a river — the surface is still, but the flow underneath continues.

The second level, timecop.DeepFreeze, completely freezes all time-related values and prevents functions like clock.Ticker, clock.After, and clock.Sleep from firing until time is moved forward. This is useful for testing components with time-sensitive behaviour. With the river example, deep freeze would mean turning a river into something like a glacier.

USAGE

package main

import (
   "go.llib.dev/testcase/assert"
   "go.llib.dev/testcase/clock"
   "go.llib.dev/testcase/clock/timecop"
   "testing"
   "time"
)

func Test(t *testing.T) {
   type Entity struct {
      CreatedAt time.Time
   }

   MyFunc := func() Entity {
      return Entity{
         CreatedAt: clock.TimeNow(),
      }
   }

   expected := Entity{
      CreatedAt: clock.TimeNow(),
   }

   timecop.Travel(t, expected.CreatedAt, timecop.Freeze)

   assert.Equal(t, expected, MyFunc())
}

Time travelling is undone as part of the test's teardown.

timecop.Travel + timecop.Freeze

The Freeze option causes the observed time to stop until the first time reading event.

timecop.SetSpeed

Let's say you want to test a "live" integration wherein entire days could pass by in minutes while you're able to simulate "real" activity. For example, one such use case is being able to test reports and invoices that run in 30-day cycles in very little time while also simulating activity via subsequent calls to your application.

timecop.SetSpeed(t, 1000) // accelerate speed by 1000x times from now on. 
<-clock.After(time.Hour) // takes only 1/1000 time to finish, not an hour.
clock.Sleep(time.Hour) // same

Design

The package uses a singleton pattern. The original design had a Clock and a Timecop type to do dependency injection, but upon doing spiking with it, it felt foreign to how we currently use time.Now() or time.After(duration). Also, it made it possible that different components reside in different timelines, while time should be observed as a singleton entity by the whole application. Time manipulation seems to be a good use case where the singleton pattern is the least wrong solution.

References

The package was inspired by travisjeffery' timecop project.

FAQ

What is the performance impact on my production code if I use clock?

There is no performance impact on your production code. The time-travel feature is only enabled during testing. In a production environment, the clock's functions act as aliases for the standard time functions.

Why not pass a function argument or time value directly to a function/method?

While injecting time as an argument or dependency is a valid approach, the aim with clock was to keep the usage feeling idiomatic and close to the standard time package, while also making testing convenient and easy.

Using dependency injection for time related components may complicate high-level testing that involve many components. In these cases, it's easier to simulate time changes with a shared clock package, rather than injecting the time component into all dependencies. This also allow your tests to use the same constructor functions as your production code and know little about which of its component is time sensitive. Also when components set fields like "CreatedAt" timestamps, it becomes very convinent to keep them in the same timeline, and make the assertions easy on the resulting entities.

For configurable values in your logic, you should still use dependency injection. However, you can efficiently test these configurations with the clock package by using time travel in your tests. For example, if you're designing a scheduler that takes time.Duration as a configuration input, you can freeze the time, set a specific duration in your test, inject it into your component, and then simulate different time scenarios based on the test cases you want to cover.

Why not just use a global variable with time.Now?

That approach can work well for testing. If you consistently use that global variable throughout your project, it can be very helpful for integration tests. This is essentially how the clock library started. As more use cases emerged during our project, we expanded it to ensure testability for those scenarios too.

If you decide to use a global variable, I highly recommend creating a Stub function for it. This function should reset the value to time.Now after the test is done, ensuring clean tests cleanups. Also, be mindful of parallel testing and potential edge cases with it.

If you're looking to create a reusable component in the form of a shared package that supports time manipulation in tests, make sure the common package that has the time stub functionality is easily accessible to those using your package.

How does the clock package help with testing time-dependent goroutine synchronization?

The clock package focuses on time manipulation rather than testing the implementation details of components using goroutines. It isn't designed to test time-based synchronization between goroutines, but rather to enable testing that focuse on the system behaviour.

For this purpose, you can use the assert.Eventually helper from testcase/assert to test systems with eventual consistency. This helper improves the likelihood that your goroutines will be scheduled more eagerly, increasing the chances that your test assertions will be met in the ideal testing time.

func TestXXX(t *testing.T) {
   subject := ...

   timecop.Travel(t, time.Hour) // trigger time related behavioural change

   assert.Eventually(t, time.Second, func(t assert.It) { // assert that eventually within a second, we expect an outcome
      got := subject.LookupSomething(...)
      assert.Equal(t, got, "expected")
   })

}
Does the clock package have a monotonic view of time?

Monotonic time means:

  • Time is always increasing steadily, without jumping backwards.
  • Every thread sees time moving forward at the same rate.
  • All goroutines share a single, unified notion of time.

The clock package inherits its time view from the standard Go time API. The Go standard library provides APIs to get the current time, which includes both the "wall clock" and the "monotonic clock". When the system clock synchronizes and changes, the wall clock time can move backward, but for measurements, Go uses the monotonic time value for precise results.

The clock package also allows traveling backward in time. In this sense, clock isn't strictly monotonic because you can set it to a specific point in time. After time travel, time is monotonic again until you again make a travel back in time.

It’s open to interpretation whether the clock remains monotonic when time is frozen with timecop.

Documentation

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func After

func After(d time.Duration) <-chan time.Time

After waits for the duration to elapse and then sends the current time on the returned channel. The underlying Timer is not recovered by the garbage collector

During testing, After will react to time travelling.

Example
package main

import (
	"testing"
	"time"

	"go.llib.dev/testcase/clock"
	"go.llib.dev/testcase/clock/timecop"
)

func main() {
	var tb testing.TB
	timecop.SetSpeed(tb, 5)    // 5x time speed
	<-clock.After(time.Second) // but only wait 1/5 of the time
}

func Now added in v0.154.0

func Now() time.Time

Now returns the current local time.

During testing, Time returned by Now is affected by time travelling.

Example
package main

import (
	"go.llib.dev/testcase/clock"
)

func main() {
	now := clock.Now()
	_ = now
}
Example (Freeze)
package main

import (
	"testing"
	"time"

	"go.llib.dev/testcase/assert"
	"go.llib.dev/testcase/clock"
	"go.llib.dev/testcase/clock/timecop"
)

func main() {
	var tb testing.TB

	type Entity struct {
		CreatedAt time.Time
	}

	MyFunc := func() Entity {
		return Entity{
			CreatedAt: clock.Now(),
		}
	}

	expected := Entity{
		CreatedAt: clock.Now(),
	}

	timecop.Travel(tb, expected.CreatedAt, timecop.Freeze)

	assert.Equal(tb, expected, MyFunc())
}
Example (WithTravelByDate)
package main

import (
	"testing"
	"time"

	"go.llib.dev/testcase/clock"
	"go.llib.dev/testcase/clock/timecop"
)

func main() {
	var tb testing.TB

	date := time.Date(2022, 01, 01, 12, 0, 0, 0, time.Local)
	timecop.Travel(tb, date, timecop.Freeze) // freeze the time until it is read
	time.Sleep(time.Second)
	_ = clock.Now() // equals with date
}
Example (WithTravelByDuration)
package main

import (
	"testing"
	"time"

	"go.llib.dev/testcase/clock"
	"go.llib.dev/testcase/clock/timecop"
)

func main() {
	var tb testing.TB

	_ = clock.Now() // now
	timecop.Travel(tb, time.Hour)
	_ = clock.Now() // now + 1 hour
}

func Since added in v0.177.0

func Since(start time.Time) time.Duration

Since returns the time elapsed since start. It is shorthand for clock.Now().Sub(t).

During testing, Since will react to time travelling.

func Sleep

func Sleep(d time.Duration)

Sleep pauses the current goroutine for at least the duration d. A negative or zero duration causes Sleep to return immediately.

During testing, it will react to time travelling events

Example
package main

import (
	"testing"
	"time"

	"go.llib.dev/testcase/clock"
	"go.llib.dev/testcase/clock/timecop"
)

func main() {
	var tb testing.TB
	timecop.SetSpeed(tb, 5)  // 5x time speed
	clock.Sleep(time.Second) // but only sleeps 1/5 of the time
}

Types

type Ticker added in v0.154.0

type Ticker = internal.Ticker

Ticker acts as a proxy between the caller and the ticker implementation. During testing, it will be a clock-based ticker that can time travel, and outside of testing, it will use the time.Ticker.

func NewTicker added in v0.154.0

func NewTicker(d time.Duration) *Ticker

NewTicker returns a new Ticker containing a channel that will send the current time on the channel after each tick. The period of the ticks is specified by the duration argument. The ticker will adjust the time interval or drop ticks to make up for slow receivers. The duration d must be greater than zero; if not, NewTicker will panic. Stop the ticker to release associated resources.

During testing, Ticker will react to time travelling.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL