Skip to main content

Stopwatch

MockTimeSystem.Stopwatch wraps System.Diagnostics.Stopwatch. It exposes both the instance API (IStopwatch: Start, Stop, Elapsed, ElapsedMilliseconds, ElapsedTicks, Restart, Reset, IsRunning) and the static API on the factory itself (IStopwatchFactory: Frequency, IsHighResolution, GetTimestamp, StartNew).

Basic usage

ITimeSystem timeSystem = new MockTimeSystem();
IStopwatch sw = timeSystem.Stopwatch.StartNew();

// Simulate work that takes 5 seconds - Sleep auto-advances the simulated clock
timeSystem.Thread.Sleep(TimeSpan.FromSeconds(5));

sw.Stop();
await Expect.That(sw.Elapsed).IsEqualTo(TimeSpan.FromSeconds(5));

Wall clock vs monotonic clock

A real Stopwatch reads the OS monotonic clock - it measures elapsed real time, completely independent of the wall-clock date. Setting the system clock backwards mid-measurement doesn't subtract from Elapsed; only actual time passing does.

MockTimeSystem mirrors that distinction. The two knobs on TimeProvider behave differently:

MethodDateTime.UtcNowRunning Stopwatch.Elapsed
AdvanceBy(TimeSpan)Moves forward by the spanMoves forward by the span
SetTo(DateTime)Jumps to the given instantUnchanged
MockTimeSystem timeSystem = new(new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc));
IStopwatch sw = timeSystem.Stopwatch.StartNew();

// Jump the wall clock backwards by a year - Stopwatch is unaffected
timeSystem.TimeProvider.SetTo(new DateTime(2025, 1, 1));
await Expect.That(sw.Elapsed).IsEqualTo(TimeSpan.Zero);

// AdvanceBy moves both the wall clock and the monotonic clock
timeSystem.TimeProvider.AdvanceBy(TimeSpan.FromMinutes(5));
await Expect.That(sw.Elapsed).IsEqualTo(TimeSpan.FromMinutes(5));

This means tests that mix Stopwatch measurements with manual SetTo calls (e.g. simulating a clock change at midnight, NTP correction, daylight-saving switch) don't produce nonsensical elapsed values. The Stopwatch always reflects "real" simulated elapsed time.

The same monotonic clock backs Timer, PeriodicTimer and Task.Delay - none of them are affected by SetTo either.

Frequency, GetTimestamp and GetElapsedTime

The static surface lives on the factory (timeSystem.Stopwatch), not on individual IStopwatch instances:

long t1 = timeSystem.Stopwatch.GetTimestamp();
timeSystem.Thread.Sleep(TimeSpan.FromSeconds(2));
long t2 = timeSystem.Stopwatch.GetTimestamp();

double seconds = (t2 - t1) / (double)timeSystem.Stopwatch.Frequency; // 2.0

On .NET 8+, GetElapsedTime does the math for you:

long start = timeSystem.Stopwatch.GetTimestamp();
timeSystem.Thread.Sleep(TimeSpan.FromSeconds(2));

TimeSpan elapsed = timeSystem.Stopwatch.GetElapsedTime(start); // 00:00:02
TimeSpan span = timeSystem.Stopwatch.GetElapsedTime(t1, t2); // 00:00:02

On the mock, Frequency is fixed at TimeSpan.TicksPerSecond * 10 = 100,000,000 (100 MHz, i.e. 10 ns per timestamp tick) and IsHighResolution is always true. The real Stopwatch.Frequency varies by hardware and OS - the mock pins it to a stable value so timestamp arithmetic in tests is deterministic.

Pause, resume, restart

Start/Stop/Reset/Restart behave exactly as on the BCL:

IStopwatch sw = timeSystem.Stopwatch.StartNew();

timeSystem.Thread.Sleep(TimeSpan.FromSeconds(3));
sw.Stop(); // Elapsed = 3s
timeSystem.Thread.Sleep(TimeSpan.FromSeconds(10)); // paused, ignored
sw.Start(); // resume
timeSystem.Thread.Sleep(TimeSpan.FromSeconds(2));
await Expect.That(sw.Elapsed).IsEqualTo(TimeSpan.FromSeconds(5));

sw.Restart(); // Elapsed = 0, running
sw.Reset(); // Elapsed = 0, stopped

A second Start() on an already-running stopwatch is a no-op, matching the BCL.