My AsyncDetector
is internally using a Stopwatch
to calculate time intervals. Because of this hardcoded dependency I am not able to write good unit-tests for it. So I must be able to use my own timestamps that I can use for assertions.
To do this I defined the IStopwatch
interface that is a simplified version of the Stopwatch
. Some of the properties I never use and my inteface does not provide them.
public interface IStopwatch
{
bool IsRunning { get; }
TimeSpan Elapsed { get; }
IStopwatch Start();
IStopwatch Stop();
IStopwatch Restart();
IStopwatch Reset();
}
Its default implementation is using of course the original Stopwatch
so I provide it just for reference:
public class SystemStopwatch : IStopwatch { private readonly Stopwatch _stopwatch; public SystemStopwatch() { _stopwatch = new Stopwatch(); } public bool IsRunning => _stopwatch.IsRunning; public TimeSpan Elapsed => _stopwatch.Elapsed; public static IStopwatch StartNew() { return new SystemStopwatch().Start(); } public IStopwatch Start() { _stopwatch.Start(); return this; } public IStopwatch Stop() { _stopwatch.Stop(); return this; } public IStopwatch Restart() { _stopwatch.Restart(); return this; } public IStopwatch Reset() { _stopwatch.Restart(); return this; } public override string ToString() { return _stopwatch.Elapsed.ToString(); } }
More interesing is the DebugStopwatch
that will enumerate my timestamps on each access to the Elapsed
property or it'll throw if there are not enough of them:
public class DebugStopwatch : IStopwatch
{
private readonly IEnumerable<TimeSpan> _timestamps;
private TimeSpan? _lastElapsed;
private IEnumerator<TimeSpan> _enumerator;
public DebugStopwatch([NotNull] IEnumerable<TimeSpan> elapses)
{
_timestamps = elapses ?? throw new ArgumentNullException(nameof(elapses));
}
public bool IsRunning { get; private set; }
public TimeSpan Elapsed
{
get
{
if (IsRunning)
{
if (_enumerator.MoveNext())
{
return (_lastElapsed = _enumerator.Current).Value;
}
throw new InvalidOperationException("You did not define enough timestamps.");
}
return _enumerator?.Current ?? _lastElapsed ?? TimeSpan.Zero;
}
}
public static IStopwatch StartNew(IEnumerable<TimeSpan> elapses)
{
return new DebugStopwatch(elapses).Start();
}
public IStopwatch Start()
{
if (IsRunning)
{
return this;
}
_enumerator = _timestamps.GetEnumerator();
IsRunning = true;
return this;
}
public IStopwatch Stop()
{
IsRunning = false;
return this;
}
public IStopwatch Restart()
{
Stop();
Reset();
Start();
return this;
}
public IStopwatch Reset()
{
_enumerator = _timestamps.GetEnumerator();
return this;
}
public override string ToString()
{
return (_lastElapsed ?? TimeSpan.Zero).ToString();
}
}
Example Using it is very easy. I just need to provide a collection or enumeration with timestamps:
void Main()
{
var stopwatch = DebugStopwatch.StartNew(Enumerable.Range(0, 10).Select(i => TimeSpan.FromSeconds(i)));
stopwatch.Dump();
stopwatch.Dump();
stopwatch.Dump();
stopwatch.Stop().Dump(nameof(IStopwatch.Stop));
stopwatch.Dump();
stopwatch.Dump();
stopwatch.Reset().Dump(nameof(IStopwatch.Reset));
stopwatch.Dump();
stopwatch.Dump();
}
What do you think of the DebugStopwatch
? Would you change anything about it? If so, how would you improve it?
1 Answer 1
I would not return IStopwatch from Start/Stop/etc. This IStopwatch is not an immutable Value Object, like String for example: it would be easier to set expectations by having just this:
public interface IStopwatch
{
bool IsRunning { get; }
TimeSpan Elapsed { get; }
void Start();
void Stop();
void Restart();
void Reset();
}
SystemStopwatch
would be a way shorter:
class SystemStopwatch : IStopwatch
{
Stopwatch Stopwatch { get; } = new Stopwatch();
public bool IsRunning => Stopwatch.IsRunning;
public TimeSpan Elapsed => Stopwatch.Elapsed;
public void Start() => Stopwatch.Start();
public void Stop() => Stopwatch.Stop();
public void Reset() => Stopwatch.Reset();
public void Restart() => Stopwatch.Restart();
}
As for the MockStopwatch
(sounds better than Debug
one for me) – it could be a little bit shorter:
class MockStopwatch : IStopwatch
{
public MockStopwatch(params TimeSpan[] intervals)
: this(intervals.AsEnumerable())
{
}
public MockStopwatch(IEnumerable<TimeSpan> intervals)
{
Interval = intervals
.Prepend(TimeSpan.Zero)
.ToList()
.GetEnumerator();
Reset();
}
IEnumerator<TimeSpan> Interval { get; }
public bool IsRunning { get; private set; }
public TimeSpan Elapsed =>
!IsRunning
? Interval.Current
: Interval.MoveNext()
? Interval.Current
: throw new InvalidOperationException("You did not define enough timestamps.");
public void Start() => IsRunning = true;
public void Stop() => IsRunning = false;
public void Restart()
{
Stop();
Reset();
Start();
}
public void Reset()
{
Interval.Reset();
Interval.MoveNext();
}
}
Where you might need to define Prepend
(.net core LINQ has one already):
static class PrependAppend
{
public static IEnumerable<T> Prepend<T>(this IEnumerable<T> source, T item) =>
Enumerable.Concat(new[] { item }, source);
public static IEnumerable<T> Append<T>(this IEnumerable<T> source, T item) =>
Enumerable.Concat(source, new[] { item });
}
Updated Elapsed
What do you say about this one? Looks cleaner for me.
public TimeSpan Elapsed => IsRunning ? NextElapsed : SameElapsed;
TimeSpan SameElapsed => Interval.Current;
TimeSpan NextElapsed => Interval.MoveNext()
? Interval.Current
: throw new InvalidOperationException("You did not define enough timestamps.");
-
\$\begingroup\$ What a compression level ;-) With methods that return the object itself it's easier to do
return new MockStopwatch().Start()
inside ofStartNew()
....Interval.MoveNext()
- this won't work. This method always throwsNotImplementedException
- I came up with the same idea yesterday - sorry, forgot to mention it. \$\endgroup\$t3chb0t– t3chb0t2017年11月13日 05:21:04 +00:00Commented Nov 13, 2017 at 5:21 -
2\$\begingroup\$ @t3chb0t 1) It is better to do not return anyway. Everyone will expect original object to stay unchanged and the modified copy to be returned... 2) I think you mean the
iterator.Reset()
method. This is why I invokeToList()
in the ctor - its iterator supports resetting. \$\endgroup\$Dmitry Nogin– Dmitry Nogin2017年11月13日 05:29:52 +00:00Commented Nov 13, 2017 at 5:29 -
\$\begingroup\$ Oh, sure, of course,
.Reset
. Ahhh, there,.ToList()
, now I see it. I couldn't decide whether I should use a list or an ienumerable I case I have an endless enumerable... I still can't :-] but maybe I should create another type for that,EndlessMockStopwatch
. \$\endgroup\$t3chb0t– t3chb0t2017年11月13日 05:32:28 +00:00Commented Nov 13, 2017 at 5:32 -
1\$\begingroup\$ I agree that void methods look better. Returning
IStopwatch
formStart
method makes it unclear whether it returns a new instance or not. However I would prefer originalDebugStopwatch.Elapsed
implementation to nested ternary operators.... \$\endgroup\$Nikita B– Nikita B2017年11月14日 08:46:53 +00:00Commented Nov 14, 2017 at 8:46 -
\$\begingroup\$ @NikitaB New
Elapsed
implementation reduces cyclomatic complexity of the method while requiring less state to preserve. \$\endgroup\$Dmitry Nogin– Dmitry Nogin2017年11月14日 13:58:51 +00:00Commented Nov 14, 2017 at 13:58
Explore related questions
See similar questions with these tags.
Restart()
inReset()
. \$\endgroup\$