10
\$\begingroup\$

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?

\$\endgroup\$
2
  • 1
    \$\begingroup\$ Your system stopwatch implementation calls Restart() in Reset(). \$\endgroup\$
    – 404
    Commented Nov 13, 2017 at 17:30
  • \$\begingroup\$ @eurotrash oops, crap, thx, I guess I hit re+tab too fast ;-) \$\endgroup\$
    – t3chb0t
    Commented Nov 13, 2017 at 17:33

1 Answer 1

11
\$\begingroup\$

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.");
\$\endgroup\$
16
  • \$\begingroup\$ What a compression level ;-) With methods that return the object itself it's easier to do return new MockStopwatch().Start() inside of StartNew().... Interval.MoveNext() - this won't work. This method always throws NotImplementedException - I came up with the same idea yesterday - sorry, forgot to mention it. \$\endgroup\$
    – t3chb0t
    Commented 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 invoke ToList() in the ctor - its iterator supports resetting. \$\endgroup\$ Commented 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
    Commented Nov 13, 2017 at 5:32
  • 1
    \$\begingroup\$ I agree that void methods look better. Returning IStopwatch form Start method makes it unclear whether it returns a new instance or not. However I would prefer original DebugStopwatch.Elapsed implementation to nested ternary operators.... \$\endgroup\$
    – Nikita B
    Commented Nov 14, 2017 at 8:46
  • \$\begingroup\$ @NikitaB New Elapsed implementation reduces cyclomatic complexity of the method while requiring less state to preserve. \$\endgroup\$ Commented Nov 14, 2017 at 13:58

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.