Przez ostatnie trzy miesiące opisywałem jak działa DSLExecutor. A jeszcze ani razu nie napisałem skąd wiem, że działa poprawnie. Jako że koniec konkursu zbliża się wielkimi krokami, pora wreszcie poruszyć ten temat. Dlatego dzisiaj opiszę, jak testuję moduły DSLExecutora.
Na początek lista bibliotek, których używam do pisania testów:
- xUnit – framework testów jednostkowych,
- Fluent Assertions – biblioteka pozwalająca wygodnie pisać czytelne asercje,
- NSubstitute – biblioteka umożliwiająca zgrabne tworzenie mocków.
Kod testów umieszczam w projekcie sąsiadującym z projektem zawierającym kod testowany. Projekt testujący nazywam zgodnie z szablonem: [nazwa projektu testowanego].Tests. Zazwyczaj tworzę jedną klasę testującą dla pojedynczej klasy testowanej. Klasy testujące umieszczam w przestrzeniach nazw analogicznych do tych, w których znajdują się klasy testowane.
Wyróżniam dwa rodzaje klas testujących:
- Klasy testujące pojedyncze klasy testowane – takie klasy zawierają typowe testy jednostkowe, w których używam mocków.
- Klasy testujące całe moduły – w nich nie używam mocków, a drzewa zależności buduję takie, jakich spodziewam się podczas rzeczywistego działania aplikacji. Takie testy tworzę dla głównych klas w projektach lub przestrzeniach nazw. Pozwalają one upewnić się, że cały moduł działa prawidłowo.
Przyjrzyjmy się bliżej obu rodzajom testów.
Testy pojedynczych klas
Jak już wspominałem, w klasach implementujących operacje umieszczam po jednej metodzie publicznej. Konsekwencją tej praktyki jest to, że w klasach je testujących wszystkie testy dotyczą pojedynczej metody. Dzięki temu kod testujący daną operację nie miesza się z kodem testującym inną.
Jako że wszystkie testy w klasie testującej testują tą samą metodę, ich kroki Act są takie same. Aby zredukować duplikację, wydzielam ten krok do osobnej metody. Przykład dla klasy testującej klasę ExpressionExecutor, czyli ExpressionExecutorTests:
object Act(IExpression expression, IConstantExpressionExecutor cee = null, IFunctionExpressionExecutor fee = null, IBatchExpressionExecutor bee = null) { var ee = new ExpressionExecutor(cee, fee, bee); return ee.Execute(expression); }
Przypadki testowe staram się nazywać opisowo (ale zwięźle) – w nazwie metody testującej umieszczam opis warunków początkowych i opis spodziewanego wyniku. W nazwach tych metod stosuję notację snake case, a pomiędzy opis warunków początkowych i opis wyników wstawiam trzy podkreślenia.
Dane wejściowe testu zazwyczaj podaję w atrybutach InlineData (więcej tutaj). Przykładowy test (sprawdzający, czy ExpressionExecutor w celu wykonania wyrażenia reprezentującego funkcję używa zależności FunctionExpressionExecutor) wygląda tak:
[Theory] [InlineData(0)] [InlineData(1)] [InlineData(5)] void FunctionExpression___invokes_functionExpressionExecutor( int expResult) { var exp = new FunctionExpression<Func, int>(); var fee = Substitute.For<IFunctionExpressionExecutor>(); fee.Execute(exp).Returns(expResult); var result = Act(exp, fee: fee); result.Should().Be(expResult); }
Testy modułów
Klasy testujące moduły są bardzo podobne do klas zawierających testy jednostkowe. Różnią się właściwie tylko tym, że nie stosuję w nich mocków. O takie testy aż prosi się klasa DSLExecutor, czyli punkt wejścia do mojego narzędzia. Przykładowo, test dowodzący, że DSLExecutor poprawnie wykonuje wyrażenia reprezentujące wartości stałe, wygląda tak:
[Theory] [InlineData(0)] [InlineData(1)] [InlineData(5)] void executes_constant_expression(int value) { var exp = new ConstantExpression<int> { Value = value }; var dslExecutor = new DSLExecutor(null); var result = dslExecutor.ExecuteExpression(exp); result.Should().Be(value); }
(Klasa DSLExecutor nie przyjmuje zależności przez konstruktor – sama sobie tworzy ich instancje.)
Tyle w temacie testów automatycznych. W kolejnym wpisie zaprezentuję wisienkę na torcie, czyli aplikację demonstrującą możliwości DSLExecutora – zapraszam!
Cześć, fajny post, ale czuję niedosyt, ponieważ testy są tematem, który bardzo lubię. Szkoda, że tak mało napisałeś na ich temat. Zastanawiam się w jaki sposób radzisz sobie z kodem powtarzającym się w wielu testach, bo zakładam że taki istnieje skoro testujesz dane funkcjonalności zarówno jednostkowo jak i integracyjne. Czy tworzysz klasy pomocnicze ze statycznymi metodami, korzystasz z klas bazowych czy może jeszcze inaczej?
Zastanawiam się również, czy pisałeś testy wydajnościowe swojej biblioteki lub planujesz to zrobić na późniejszym etapie projektu? Dla każdego, który chciałby z niej skorzystać rzetelną informacją byłyby wyniki testów pokazujące jak bliboteka radzi sobie zarówno z małymi jak i ogromnymi drzewami. Poza tym testy wydajnościowe niosłyby wartość w przyszłości przy refaktoryzacjach kodu, ponieważ miałbyś informację o tym, czy dokonane zmiany spowodowały wzrost czy spadek wydajności biblioteki.
Siema mój jedyny (chyba) Czytelniku, szkoda że tym razem Cię zawiodłem
Wiem, że nie poruszyłem tematów omawianych w innych postach / artykułach. Ale wolałem właśnie napisać o czymś, czego nie spotkałem nigdzie indziej – o organizacji testów w projektach / folderach / plikach i o upewnianiu się, że całe moduły (a nie tylko odizolowane jednostki) systemu działają prawidłowo.
Co do reużywalnego kodu w testach:
W xUnit dostępne są takie oto możliwości: http://xunit.github.io/docs/shared-context.html. Jednak wszystkie one dotyczą tworzenia stanu współdzielonego przez wiele testów w pojedynczym uruchomieniu runnera. Jeszcze nie spotkałem się z potrzebą stosowania takich czarów (staram się aby testy były uruchamiane w izolacji). Po prostu wydzielam zwykłe klasy implementujące powtarzające się kawałki, a instancje tych klas tworzę w konstruktorach klas testujących. Czasem wydzielam klasy statyczne zawierające extension methods.
Co do testów wydajności:
Zdaję sobie sprawę, że takie testy byłyby przydatne, ale na razie nie zajmowałem się wydajnością DSLExecutora. I niestety nie zdążę już tego zrobić w ramach “Daj Się Poznać” – brak czasu zmusił mnie do okrojenia zamierzonych funkcjonalności (i wymagań niefunkcjonalnych). W poście podsumowującym konkurs będę się gęsto tłumaczył…