I've been playing around with a concept of refactoring the static methods to invoke a delegate which you can externally set for testing purposes.
This would not use any testing framework and would be a completely bespoke solution however the refactor will not influence the signature of your caller and so it would be a relatively safe.
For this to work, you would need to have access to the static method, so it wouldn't work for any external libraries such as System.DateTime
.
Heres an example I've been playing with where I've created a couple of static methods, one with a return type that takes in two parameters and one generic which has no return type.
The main static class:
public static class LegacyStaticClass
{
// A static constructor sets up all the delegates so production keeps working as usual
static LegacyStaticClass()
{
ResetDelegates();
}
public static void ResetDelegates()
{
// All the logic that used to be in the body of the static method goes into the delegates instead.
ThrowMeDelegate = input => throw input;
SumDelegate = (a, b) => a + b;
}
public static Action<Exception> ThrowMeDelegate;
public static Func<int, int, int> SumDelegate;
public static void ThrowMe<TException>() where TException : Exception, new()
=> ThrowMeDelegate(new TException());
public static int Sum(int a, int b)
=> SumDelegate(a, b);
}
The Unit Tests (xUnit and Shouldly)
public class Class1Tests : IDisposable
{
[Fact]
public void ThrowMe_NoMocking_Throws()
{
Should.Throw<Exception>(() => LegacyStaticClass.ThrowMe<Exception>());
}
[Fact]
public void ThrowMe_EmptyMocking_DoesNotThrow()
{
LegacyStaticClass.ThrowMeDelegate = input => { };
LegacyStaticClass.ThrowMe<Exception>();
true.ShouldBeTrue();
}
[Fact]
public void Sum_NoMocking_AddsValues()
{
LegacyStaticClass.Sum(5, 6).ShouldBe(11);
}
[Fact]
public void Sum_MockingReturnValue_ReturnsMockedValue()
{
LegacyStaticClass.SumDelegate = (a, b) => 6;
LegacyStaticClass.Sum(5, 6).ShouldBe(6);
}
public void Dispose()
{
LegacyStaticClass.ResetDelegates();
}
}