AsynchrounouslyGetUser…czyli piszemy krótki test konwencji z xUnit i ASP.NET Core

Cześć!

Dziś wpis, który powstał przez przypadek. Jak mawia stare chińskie przysłowie:

 

There are only two hard things in Computer Science: cache invalidation and naming things.

 

Taaaak, każdy to zna 😉 Pytania o to jak nazwać zmienną, metodę czy klasę to standard jeśli chodzi o programowanie. Wynika to z kilku rzeczy. Po pierwsze chcemy mieć pewność, że nazwa dokładnie oddaje istotę danego „bytu”, po drugie każdy ma swoje standardy nazewnictwa, które mogą nie być tak oczywiste dla innych jak dla nas. I to jest ok. Nie możemy przecież wymagać, aby każdy myślał dokładnie tak samo. Z drugiej strony samowolka w nazewnictwie prowadzi do tego, że w którymś momencie nie wiemy co czytamy. Raz widzimy IUserRespository, a już kilka plików dalej przychodzi nam czytać CompanyRepositoryInterface (może lekko ekstremalny przykład 😉 ). Warto zatem wypracować z zespołem konwencję nazewnictwa tak, aby każdy wiedział co jest czym, a ponadto wiedziały to osoby, które przyjdą za kilka lat utrzymywać system. Do tego celu bardzo dobrze sprawdzają się testy konwencji. Wspomniałem na początku, że post powstał trochę przez przypadek, a to dlatego, że ja osobiście nie planowałem ich w swoim projekcie konkursowym. Po pierwsze jestem jednoosobową drużyną A, a po drugie byłem przekonany, że żadnych standardów nazewnictwa póki co nie złamałem. Żeby jednak utwierdzić się w tym przekonaniu postanowiłem taki mały test napisać. Ofiarą zostały metody asynchroniczne. Ja osobiście jestem bardzo uczulony na to, aby każda z nich posiadała sufiks „Async„. Powód jest prosty, bardzo łatwo jest przeoczyć uwagę w popupie (awaitable). Konsekwencje mogą być poważne, a i znalezienie błędu też nie należy do najprostszych. No dobra, czas na odrobinę kodu. Pierwszą rzeczą jaką musimy zrobić to oczywiście utworzenie projektu z testami i pobranie xUnit (to znaczy ja go używam, możecie użyć czegoś innego). W tym celu w pliku project.json musimy dodać następujące linijki:

 


"dependencies": {
   //inne zależności
    "xunit": "2.1.0-*",
    "xunit.runner.dnx": "2.1.0-*"
 
  },
 
  "commands": {
    "test": "xunit.runner.dnx"
  }

 

Po zapisaniu pliku jesteśmy gotowi do działania. Jak więc sprawdzimy, czy wszystkie metody asynchroniczne posiadają odpowiednie nazwy? Z wybranego assembly pobierzemy wszystkie klasy oraz interfejsy, a następnie wszystkie metody, które są w nich zawarte. Następnie otrzymany zbiór przefiltrujemy tak, aby zwrócone zostały tylko te metody, które zwracają Task<TResult> i nie zawierają sufiksu Async. W fazie Assert sprawdzimy czy otrzymany zbiór wynikowy jest pusty. Aha, jeszcze mała uwaga. Ktoś mógłby mi powiedzieć, że metoda asynchroniczna może zwracać void. Tu moja odpowiedź, dlaczego tego nie sprawdzam 😀 Ok, pobieramy assembly:

 


public class TestHelper
{
    public static Assembly GetAssembly(string name)
    {
        return PlatformServices.Default.LibraryManager.GetLibraries().SelectMany(l => l.Assemblies.Select(an =>
        {
            try
            {
                return Assembly.Load(an);
            }
            catch (ReflectionTypeLoadException)
            {
                return null;
            }
        })).FirstOrDefault(a => a != null && a.FullName.Contains(name));
    }

 

Jak widać, korzystamy tu z refleksji. Ponadto metoda została „wyciągnięta” do helpera, żeby nie powielać bezcelowo kodu. Kolejnym etapem jest napisanie metody, która zwróci nam kolekcję obiektów MethodInfo, z których dowiemy się o zwracanym typie metody oraz jej nawie. Napiszemy w tym celu metodę rozszerzającą:

 


public static class TestsExtensions
{
    public static IEnumerable<MethodInfo> GetInvalidAsyncMethods(this Type[] that)
    {
        return that.SelectMany(c => c.GetMethods())
            .Where(m => m.ReturnType.IsAssignableFrom(typeof(Task<>)) && !m.Name.EndsWith("Async"));
    }
}

 

Tu warto zwrócić uwagę, że nie używamy metody Contains, a EndsWith. Dzięki temu test nie zakończy się sukcesem jeśli metoda będzie nazywała się AsynchrounouslyGetUser. Kiedy oba elementy zostały zaimplementowane, czas napisać sam test:

 


public class AsynchronousMethodsNaming
{
    [Fact]
    public void each_asynchronous_method_in_Domain_namespace_contains_async_suffix()
    {
        var async_methods_without_async_suffix = Execute(AssemblyNames.Domain);

        Assert.Empty(async_methods_without_async_suffix);
    }


    [Fact]
    public void each_asynchronous_method_in_DomainProxy_namespace_contains_async_suffix()
    {
        var async_methods_without_async_suffix = Execute(AssemblyNames.DomainProxy);

        Assert.Empty(async_methods_without_async_suffix);
    }

    [Fact]
    public void each_asynchronous_method_in_Web_namespace_contains_async_suffix()
    {
        var async_methods_without_async_suffix = Execute(AssemblyNames.Web).Where(m => !m.Name.Contains("ViewBag"));

        Assert.Empty(async_methods_without_async_suffix);
    }

    private static IEnumerable<MethodInfo> Execute(string @namespace)
    {
        var domain_assembly = TestHelper.GetAssembly(@namespace);

        var domain_types = domain_assembly.GetTypes();
        return domain_types.GetInvalidAsyncMethods();
    }
}

 

Jak widać, tu konwencja nazewnictwa jest zupełnie inna. Nie używamy prawilnego PascalCase, a snake_case. Wynika to z faktu, że nazwy metod testujących muszą dobrze opisywać scenariusz, których pokrywamy testami. Moim zdaniem nazwa EachAscynchronousMethodInWebNamepsaceConatinsAsyncSuffix jest mało czytelna, ale co kto lubi 😀 Nasza klasa zawiera po jednym teście, dla każdej warstwy w projekcie. Teoretycznie moglibyśmy ująć to w ramach jednej z trzema Assert-ami, ale taki sposób wydaje mi się mało praktyczny, ponieważ w przypadku niepowodzenia nie wiemy gdzie szukać winowajcy. Cała „logika” testów zawiera się w metodzie Execute (Procent stajl). No dobrze, cofnijmy się o 24 godziny kiedy to pewny siebie odpaliłem testy. W tym celu będąc w katalogu głównym projektu Tests wpisałem:

dnx test

Po chwili ujrzałem rezultat:

 

test_fail

 

Moja mina wyglądała mniej więcej tak:

 

reaction

 

Jak widać w sumie popełniłem cztery gafy. Dwie w interfejsie oraz dwie w klasie (wypisałem sobie te metody w konsoli). Morał historii jest krótki: nie ufajcie nikomu, a najbardziej sobie 🙂 Po szybkiej zmianie i uruchomieniu testów ponownie, rezultat był już taki, jakiego się spodziewałem:

 

test_success

 

Kod, który Wam przedstawiłem to jeden z wielu scenariusz do testowania. Jeżeli temat Was zaciekawił zachęcam do obejrzenia prezentacji Maćka Aniserowicza, którą przedstawił na dotNetConfPL. Link macie tu. Moim zdaniem, każdy powinien dobierać tego typu testy pod swój zespół. Jeżeli przyjdzie nam współpracować ze świeżo upieczonymi programistami możemy pokusić się o testy sprawdzające dosłownie wszystko: od tego czy interfejsy zaczynają się od wielkiej litery „I” po sprawdzaniu, czy każda klasa implementuje jakiś interfejs, lub jest zarejestrowana w kontenerze IoC. Możliwości macie miliony.

Na dziś to tyle. Zachęcam Was do śledzenie mojego twittera, gdzie wrzucam najnowsze wpisy i inne ciekawostki ze świata IT. Jeżeli wpis się podobał możecie wpaść także na facebooka i zostawić kciuka 🙂

No to cześć !

You may also like...