5
\$\begingroup\$

I'd like to be able to better test application stability. Usually when you're doing this, you run the application and keep your fingers crossed that it won't crash when an error occurs (be it a missing file, no database connection or whatever service didn't work).

The problem is that it's difficult to provoke everything that can go wrong. So instead of disabling services or removing files etc. I'd like to throw diagnostic exceptions that would pretend to be real errors.


Implementation

I call my solution PhantomException that is represented by this simple interface:

public interface IPhantomException
{
    void Throw(string name, string message = default);
}

It's implemented as a small service that gets injected to whatever specifies this as a dependency.

public class PhantomException : IPhantomException, IEnumerable<IPhantomExceptionPattern>
{
    private readonly IList<IPhantomExceptionPattern> _patterns = new List<IPhantomExceptionPattern>();

    public void Throw(string name, string message = default)
    {
        lock (_patterns)
        {
            var matches = _patterns.Where(t => t.Matches(name)).Join(", ");
            if (matches.Any())
            {
                throw DynamicException.Create
                (
                    name ?? "Phantom",
                    message ?? $"This phantom exception was thrown because it matches [{matches}]."
                );
            }
        }
    }

    public void Add(IPhantomExceptionPattern pattern) => _patterns.Add(pattern);

    public IEnumerator<IPhantomExceptionPattern> GetEnumerator() => _patterns.GetEnumerator();

    IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_patterns).GetEnumerator();
}

The service requires a collection of patterns represented by an interface

public interface IPhantomExceptionPattern : IDisposable
{
    bool Matches(string name);
}

and a default implementation. It queries values from the collection and tests each of them if it's matched. A match means an exception can be thrown.

public abstract class PhantomExceptionPattern<T> : IPhantomExceptionPattern, IDisposable
{
    private IEnumerator<T> _values;

    protected PhantomExceptionPattern(IEnumerable<T> values)
    {
        _values = values.GetEnumerator();
        try
        {
            if (!_values.MoveNext())
            {
                throw new ArgumentException
                (
                    paramName: nameof(values),
                    message: $"Cannot initialize '{GetType().ToPrettyString()}' because there has to be at least one value."
                );
            }
        }
        catch (Exception)
        {
            _values.Dispose();
            throw;
        }

        Reset();
    }

    [CanBeNull]
    public Func<string, bool> Predicate { get; set; }

    protected T Current => _values.Current;

    protected bool Eof => _values is null;

    public bool Matches(string name)
    {
        if (Eof) return false;
        if (Predicate?.Invoke(name) == false) return false;
        if (Matches() == false) return false;

        Reset();

        if (!_values.MoveNext())
        {
            _values.Dispose();
            _values = null;
        }

        return true;
    }

    protected abstract bool Matches();

    protected abstract void Reset();

    public abstract override string ToString();

    public void Dispose() => _values?.Dispose();
}

Matching of each value is passed to a concrete pattern. I have two of them:

public class CountPattern : PhantomExceptionPattern<int>
{
    private int _counter;

    public CountPattern(IEnumerable<int> values) : base(values) { }

    protected override bool Matches() => ++_counter == Current;

    protected override void Reset() => _counter = 0;

    public override string ToString() => $"{nameof(CountPattern)}: {_counter}";
}

public class IntervalPattern : PhantomExceptionPattern<TimeSpan>
{
    private Stopwatch _stopwatch;

    public IntervalPattern(IEnumerable<TimeSpan> values) : base(values) { }

    protected override bool Matches() => _stopwatch.Elapsed >= Current;

    protected override void Reset() => _stopwatch = Stopwatch.StartNew();

    public override string ToString() => $"{nameof(IntervalPattern)}: {Current} at ({_stopwatch.Elapsed})";
}

The first one matches every n-call and the other one after the specified time. For each pattern a predicate can be specified to filter exceptions that should be thrown. If nothing is specified then everything matches. When the collection of values is exhausted, the enumerator is disposed and nulled. A pattern in this state (Eof) return always false


Examples

This shows how I use it:

public class PhantomExceptionTest
{
    [Fact]
    public void Can_throw_by_CountPattern()
    {
        var phantomException = new PhantomException
        {
            new CountPattern(Sequence.Constant(2))
            {
                Predicate = name => name == "TooFast"
            }
        };

        var counts = new List<int>();
        foreach (var n in Sequence.Monotonic(0, 1).Take(10))
        {
            try
            {
                phantomException.Throw("TooFast");
            }
            catch (DynamicException ex) when (ex.NameMatches("^TooFast"))
            {
                counts.Add(n);
            }
        }

        Assert.Equal(Sequence.Monotonic(1, 2).Take(5), counts);
    }

    [Fact]
    public async Task Can_throw_by_IntervalPattern()
    {
        var phantomException = new PhantomException
        {
            new IntervalPattern(Sequence.Constant(TimeSpan.FromSeconds(2)))
            {
                //Predicate = // not using here
            }
        };

        var counts = new List<TimeSpan>();
        foreach (var n in Sequence.Constant(TimeSpan.FromSeconds(2.5)).Take(2))
        {
            await Task.Delay(n);

            try
            {
                phantomException.Throw("TooFurious");
            }
            catch (DynamicException ex) when (ex.NameMatches("^TooFurious"))
            {
                counts.Add(n);
            }
        }

        Assert.Equal(2, counts.Count);
    }
}

Bonus - helper sequences

My tests here use Sequence helpers for generating collections. Here they are.

This is the main abstraction that provide reusable code for concrete implementations:

public abstract class Sequence<T> : IEnumerable<T>
{
    private readonly IEnumerable<T> _value;

    protected Sequence(IEnumerable<T> value) => _value = value;

    #region IEnumerable

    public virtual IEnumerator<T> GetEnumerator() => _value.GetEnumerator();

    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

    #endregion
}

public abstract class Sequence
{
    public static IEnumerable<T> Constant<T>(T value) => new ConstantSequence<T>(value);

    public static IEnumerable<int> Random(int min, int max) => new RandomSequence(min, max);

    public static IEnumerable<T> Fibonacci<T>(T one) => new FibonacciSequence<T>(one);

    public static IEnumerable<T> Monotonic<T>(T start, T step) => new MonotonicSequence<T>(start, step);

    public static IEnumerable<T> InfiniteDefault<T>()
    {
        using (var e = new InfiniteDefaultEnumerator<T>())
        {
            while (e.MoveNext())
            {
                yield return default;
            }
        }
    }    
}

public class InfiniteDefaultEnumerator<T> : IEnumerator<T>
{
    public T Current => default;

    object IEnumerator.Current => Current;

    public bool MoveNext() => true;

    public void Reset() { }

    public void Dispose() { }
}

Sequence implementations:

public class ConstantSequence<T> : Sequence<T>
{
    public ConstantSequence(T value) : base(Sequence.InfiniteDefault<T>().Select(_ => value)) { }
}

public class FibonacciSequence<T> : Sequence<T>
{
    public FibonacciSequence(T one) : base(Create(one)) { }

    private static IEnumerable<T> Create(T one)
    {
        yield return one;

        var previous = one;
        var current = one;

        foreach (var _ in Sequence.InfiniteDefault<T>())
        {
            yield return current;

            var newCurrent = BinaryOperation<T>.Add(previous, current);
            previous = current;
            current = newCurrent;
        }
    }
}

public class MonotonicSequence<T> : Sequence<T>
{
    public MonotonicSequence(T start, T step) : base(Create(start, step)) { }

    private static IEnumerable<T> Create(T start, T step)
    {
        foreach (var _ in Sequence.InfiniteDefault<T>())
        {
            yield return start;
            start = BinaryOperation<T>.Add(start, step);
        }
    }
}

public class RandomSequence : Sequence<int>
{
    public RandomSequence(int min, int max, Func<int, int, int> next)
        : base(Sequence.InfiniteDefault<int>().Select(_ => next(min, max))) { }

    public RandomSequence(int min, int max)
        : this(min, max, CreateNextFunc((int)DateTime.UtcNow.Ticks)) { }

    private static Func<int, int, int> CreateNextFunc(int seed)
    {
        var random = new Random(seed);
        return (min, max) => random.Next(min, max);
    }
}

Questions

Would you say this code is easy to use? How about its implementation? Any other thoughts?

\$\endgroup\$
7
  • \$\begingroup\$ How would you use this in application code? Would you stub a dependency in a unit test and throw a phantom exception or would you paste this in your code while debugging? This part isn't that clear to me. \$\endgroup\$
    – dfhwze
    Jun 28, 2019 at 16:59
  • \$\begingroup\$ @dfhwze I inject IPhantomException and call in various places pe.Throw("Name1") and pe.Throw("Name2") etc. if some filter match than an excepiton is thrown. In production the pattern collection is empty. \$\endgroup\$
    – t3chb0t
    Jun 28, 2019 at 17:00
  • 1
    \$\begingroup\$ Imagine there are traps inside your application that you can activate for testing. This testing is running the application as normal but with activated fake faults. \$\endgroup\$
    – t3chb0t
    Jun 28, 2019 at 17:06
  • \$\begingroup\$ Would you care to provide such an example where you inject it? I would like to figure out whether this pollutes code or is acceptable in production code. \$\endgroup\$
    – dfhwze
    Jun 28, 2019 at 17:08
  • 1
    \$\begingroup\$ @dfhwze this will be difficult as this part of the code I cannot reveal but it's basically the same as when you use dependency injection for any other service via constructor: FileReader(ILogger logger, IPhantomException phantomException) then later an API of it does string ReadFile(string name) { _phantomException.Throw("FileNotFound"); _logger.Log(...); ... - nothing fancy; You use it in the same way as a logger that can also be deactivated. \$\endgroup\$
    – t3chb0t
    Jun 28, 2019 at 17:13

0

Your Answer

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