Automatyczne testowanie guard clauses w konstruktorach

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:

  • AutoFixture.Idioms
  • AutoFixture.AutoMoq

W przykładowym kodzie użyjemy:

  1. NUnitowego TestCaseSource do definicji wszystkich typów klas, których konstruktory chcemy sprawdzić.
  2. Klasy GuardClauseAssertion, która pomoże nam w tytułowym rozwiązaniu problemu.
  3. Klasy 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:

  • Analizowane są wszystkie publiczne konstruktory.
  • W analizie nie są brane null-checki dla typów 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

Opublikowano 14 września 2019

Blog o programowaniu
Dariusz Woźniak · GitHub · LinkedIn · Twitter · Goodreads