W testach bardzo często mamy do czynienia z powtarzaniem tego samego kodu. Tak samo ma się sprawa z testowaniem null-checków (zw. inaczej jako guard clauses) w konstruktorach. Jeśli chcemy napisać test dla takiego przypadku, to zazwyczaj jest on napisany jako osobna metoda. Jak możemy sobie uprościć życie? Z pomocą przychodzi biblioteka AutoFixture.Idioms.
Aby móc skorzystać z funkcji do automatycznego testowania null-checków w konstruktorach, potrzebujemy zainstalować dwa nugety:
W przykładowym kodzie użyjemy:
TestCaseSource
do definicji wszystkich typów klas, których konstruktory chcemy sprawdzić.GuardClauseAssertion
, która pomoże nam w tytułowym rozwiązaniu problemu.AutoMoqCustomization
, której celem jest automockowanie parametrów konstruktora.Kod testowy wygląda następująco:
public class Tests
{
private static IEnumerable<Type> TypesToTest()
{
var selectMany = Assembly.Load("MyAssembly.ProjectName").GetTypes();
var types = selectMany.Where(t => t.IsClass &&
t.Namespace != null &&
t.Namespace.StartsWith("MyAssembly.ProjectName"));
foreach (var type in types) yield return type;
}
[TestCaseSource(nameof(TypesToTest))]
public void AllConstructorsMustBeGuardClaused(Type type)
{
var fixture = new Fixture().Customize(new AutoMoqCustomization());
var assertion = new GuardClauseAssertion(fixture);
assertion.Verify(type.GetConstructors());
}
}
Gdzie klasa SomeClass
to:
(dla poniższego kodu powyższy test będzie zielony ✅)
public class SomeClass
{
private readonly I1 _i1;
private readonly I2 _i2;
private readonly I3 _i3;
private readonly int? _nullablei;
private readonly string _s;
public SomeClass(I1 i1, I2 i2, I3 i3)
{
_i1 = i1 ?? throw new ArgumentNullException(nameof(i1));
_i2 = i2 ?? throw new ArgumentNullException(nameof(i2));
_i3 = i3 ?? throw new ArgumentNullException(nameof(i3));
}
public SomeClass(I1 i1)
{
_i1 = i1 ?? throw new ArgumentNullException(nameof(i1));
}
}
public interface I1 { }
public interface I2 { }
public interface I3 { }
Kilka uwag:
Nullable<>
.Jeśli zabraknie nam null-checków, to test wyrzuci bardzo jasny komunikat:
AutoFixture.Idioms.GuardClauseException : An attempt was made to assign the value null to the parameter "i1" of the method ".ctor", and no Guard Clause prevented this. Are you missing a Guard Clause?
Jak widać, możemy sobie w bardzo prosty zautomatyzować testy konstruktorów i definiować wszystkie klasy z konstruktorami wymagającymi null-checki w jednym miejscu.
Pytanie — Czy aby na pewno potrzebujemy testować null-checki w konstruktorach? Tak! Guard clause mają na celu tzw. fail fast, czyli zgłosić błąd jak najszybciej. Jeśli parametr konstruktora będzie nullem, a kod będzie wymagać tego parametru, to dzięki defensywnemu podejściu, dostaniemy szybciej informację zwrotną o błędzie. Późniejszy błąd może mieć również skutki uboczne, jeśli na przykład przed wystąpieniem błędu zmutowano stan obiektu. A zatem, skoro sam kod jest potrzebny, testy tym bardziej!
Kod źródłowy do przykładu znajduje się na GitHubie: https://github.com/dariusz-wozniak/AutoTestGuardClausesDemo