Świat nie jest idealny tak samo, jak kod źródłowy programisty. Czy możemy sobie i zespołowi pomóc w codziennej pracy nad produktem zgodnie z przyjętymi standardami wytwarzania oprogramowania? Śmiało powiedzmy tak, chwila na zastanowienie i nastała cisza … W zespole powinna być przyjęta konwencja kodowania, którą zespół powinien przestrzegać w celu zapewnienia jednolitości kodu w projekcie. W celu realizacji tej potrzeby przydatne będą testy konwencji.

Testy konwencji

Na początek zastanówmy się, kiedy warto stosować testy konwencji i jaką wartość możemy wnieść implementując powyższe testy? Pierwsza myśl, jaka przychodzi mi do głowy to wprowadzenie świeżej osoby (nowy pracownik, stażysta, praktykant, osoba z innego zespołu) do aktualnie rozwijanego projektu. Każda nowa osoba potrzebuje czasu, by przywyknąć do standardów kodowania, które mogą różnić się od wcześniej im znanych. Uruchamiając testy konwencji lokalnie, możemy przed wypchnięciem kodu do zdalnego repozytorium zweryfikować czy wytwarzany kod jest zgodny z przyjętymi standardami. Alternatywą dla testów konwencji są narzędzia do statycznej analizy kodu.

Przykłady

Testy konwencji opierają się na mechanizmie refleksji. Z danego assembly wykorzystując refleksje uzyskujemy dostęp do szczegółowych informacjach o typach. Przejdźmy do pierwszego przykładu zaimplementowanego z wykorzystaniem NUnit. Jesteśmy leniwi i chcemy w naszym API zautomatyzować rejestracje instancji typu Repository w kontenerze IoC Autofac. W tym celu zaimplementowałem klasę RepositoryModule, której metoda odpowiada za wyszukiwanie i rejestracje zestawu typów zgodnie z określoną regułą (instancja implementuje marker interface IRepository). Pierwszy test zweryfikuje czy wszystkie interfejsy typu Repository ze wskazanej przestrzeni nazw dziedziczą marker interface IRepository

using System.Reflection;
using Api.Interfaces.Markers;
using Autofac;

namespace Api.IoC
{
    public class RepositoryModule : Autofac.Module
    {
        protected override void Load(ContainerBuilder builder)
        {
            var assembly = typeof(RepositoryModule).GetTypeInfo().Assembly;

            builder.RegisterAssemblyTypes(assembly)
                .Where(x => x.IsAssignableTo())
                .AsImplementedInterfaces()
                .InstancePerLifetimeScope();
        }
    }
}
[Test]
public void When_RepositoryInterface_Expect_RepositoryInterfaceInheritsFromMarkerInterface()
{
	var types = typeof(RepositoryModule).Assembly.GetTypes();

	var interfacesThatDoesNotInheritFromMarkerInterface = types
		.Where(t => t.Namespace.Equals("Api.Interfaces.Repositories"))
		.Where(t => t.IsInterface)
		.Where(i => i.GetInterface(nameof(IRepository)) == null)
		.ToList();

	Assert.IsEmpty(interfacesThatDoesNotInheritFromMarkerInterface);
}

W przypadku prawidłowej implementacji w ramach testu powinniśmy uzyskać pustą kolekcję. Przejdźmy do drugiego analizowanego przypadku. Załóżmy, że posiadamy metodę o następującej sygnaturze public async Task<int> SomeMethodAsync() łatwo zapomnieć o Async, albo popełnić literówkę np. Asycn. Wesołe dinozaury programowania także takie błędy popełniają, nie tylko nowicjusze. Jak możemy się przed tym zabezpieczyć? Napiszmy test konwencji, który zweryfikuje czy w naszym API istnieją metody asynchroniczne nieposiadające suffixu Async.

[Test]
public void When_AsyncMethod_Expect_TheMethodNameEndsWithTheAsyncSuffix()
{
	var methods = typeof(RepositoryModule).Assembly
		.GetTypes()
		.SelectMany(t => t.GetMethods(BindingFlags.NonPublic | BindingFlags.Public
									  | BindingFlags.Instance | BindingFlags.Static |
									  BindingFlags.DeclaredOnly))
		.ToList();

	var asyncMethodsWithoutAsyncSuffix = methods
		.Where(m => m.GetCustomAttribute() != null)
		.Where(m => m.Name.EndsWith("Async") == false)
		.ToList();

	Assert.IsEmpty(asyncMethodsWithoutAsyncSuffix);
}

Analogicznie jak w poprzednim teście w asercji sprawdzamy, czy kolekcja jest pusta. Zatrzymam się na trzech przykładach i na koniec sprawdzę, czy w API każda klasa Controller dziedziczy po klasie ControllerBase.

[Test]
public void When_Controller_Expect_ControllerInheritsFromControllerBase()
{
	var types = typeof(RepositoryModule).Assembly.GetTypes();

	var controllersThatDoesNotInheritControllerBase = types
		.Where(t => t.Name.EndsWith("Controller")) 
		.Where(t => t.IsClass)
		.Where(c => c.IsSubclassOf(typeof(ControllerBase)) == false)
		.ToList();
	
	Assert.IsEmpty(controllersThatDoesNotInheritControllerBase);
 }

Jeśli otrzymaliście przy wszystkich testach konwencji kolor zielony, zadanie można uznać za zakończone sukcesem. W ramach dbania o czytelność testów warto kilka operacji wydzielić do extension method i także ograniczyć ilość wywołań where poprzez wykorzystanie operatora &.

Podsumowanie

Testy konwencji i narzędzia do statycznej analizy kodu wymuszają stosowanie przyjętych standardów dla kodu źródłowego w projekcie. Powyższe trzy przykłady to tylko początek przygody z testami. W ramach waszych projektów wystąpi wiele scenariuszy, których przestrzeganie ustrzeże zespól przed błędami w systemie i zapewni jednolitość kodu w projekcie.