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?
IPhantomException
and call in various placespe.Throw("Name1")
andpe.Throw("Name2")
etc. if some filter match than an excepiton is thrown. In production the pattern collection is empty. \$\endgroup\$FileReader(ILogger logger, IPhantomException phantomException)
then later an API of it doesstring 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\$