Emoji-Driven Development | Jak skutecznie zachęcić zespół do pisania (i czytania!) testów jednostkowych dzięki DSL

zycienakodach.pl 3 lat temu

Z życia na kodach

Z wielką satysfakcją postawił właśnie ostatni średnik i zakończył swoją wspaniałą implementację. Wszystkie zmienne i funkcje jasno nazwane. O tak! Stężenie zastosowanych wzorców projektowych większe niż midichlorianów we krwi Anakina Sywalkera? Jeszcze jak! Jeśli Uncle Bob, myślał o jakimś konkretnym kodzie, pisząc swoją książkę „Clean Code”, to było to właśnie to, co przed chwilą wyszło spod jego palców. Senior developer jak ta lala!

Pobieranie próbki krwi do badania stężenia midichlorianów. Gwiezdne Wojny: Mroczne Widmo.

Zostało jeszcze tylko jedno… Kliknął „Run all tests”. I niestety… nie tylko ekran, ale cały świat zdawał mu się, jakby nabrał bardziej czerwonych barw. Monitor zaroił się od przeróżnych błędów. More argument expected, is not a function… Rób TDD — mówili… Red-Green-Refactor mówili… Będzie fajnie — mówili…

TL; DR

Poprzez zastosowanie Domain-Specific Language do testów jednostkowych możemy upiec dwie pieczenie na jednym ogniu. Porównując do dylematu z Heroes III, kiedy znajdujesz skrzynię… Tym razem możesz wybrać na raz doświadczenie i złoto. Takie testy będą nie tylko czytelne dla ekspertów domenowych, ale też łatwe w rozwoju i utrzymaniu. Proste i zrozumiałe definiowanie nowych przypadków testowych z pewnością zachęci developerów do ich tworzenia. Jeśli masz już trochę doświadczenia w branży, to z pewnością zdajesz sobie sprawę z tego, iż testy są wielokrotnie częściej czytane niż pisane (jak, i cała reszta kodu). Dlatego zapewnienie ich czytelności jest niezmiernie ważne w każdym projekcie.

Kod w życiu, życie na kodach… a co z życiem w kodzie?

Mój nauczyciel fizyki zawsze mówił, iż gdy wykonuje zwykłe, codzienne czynności wszędzie widzi oddziaływające siły obiektów. Jeśli jesteś programistą, to w swoim życiu na kodach nieustannie widzisz gdzieś… no właśnie — kod. Blue screen na automacie z biletami? Normalka… Błąd 401 na wyświetlaczu kiosku w McDonaldzie? To też już Ciebie nie dziwi.

Zamawianie jedzenia czy zakup biletów zostały już dawno zautomatyzowane. Choć są to proste czynności, to jak widzisz, wszędzie zdarzają się błędy.

Teraz osoby takie jak Elon Musk odnoszą sukcesy w bardziej spektakularnych kwestiach jak autonomiczne samochody. Zagłębmy się na chwilę w podobną tematykę…

Niezależnie czy masz prawo jazdy, czy też nie, ze znakami drogowymi spotykasz się na co dzień. Galopując swoimi końmi mechanicznymi, mijasz je w mgnieniu oka i choćby za bardzo się nad nimi nie zastanawiasz. A i tak wiesz, jak jechać poprawnie z przepisami, czy jaka jest dozwolona prędkość (chociaż to jedyny znak, który sam niestety jakoś mimowolnie często przeoczam).

Znaki drogowe F-10 (kierunki na pasach ruchu), T-6C (tabliczka wskazująca rzeczywisty przebieg drogi z pierwszeństwem przez skrzyżowanie), F-8 (wyznaczony objazd).

Gdybyś spojrzał na znak przedstawiony po lewej przez ułamek sekundy, to prawdopodobnie wiesz, z których pasów możesz jechać prosto. Z łatwością odtwarzasz znane dla Twojego mózgu wzorce i zachowujesz się odpowiednio. Nie pojawiają się nam nagle myśli (chyba iż jesteś instruktorem nauki jazdy): „No tak, właśnie mijam znak F-10, który pokazuje kierunki na pasach ruchu”. Ich przeprocesowanie w naszej pamięci operacyjnej, zajęłoby o wiele więcej czasu, niż ten wymagany na podjęcie decyzji.

Zróbmy teraz mały eksperyment. Załóżmy, iż znaki drogowe nie mają jednak ilustracji, a są jedynie w formie tablic zapełnionych tekstem. Ile na poniższym przykładzie zajmie Ci zrozumienie, jakie są dozwolone kierunki jazdy z danych pasów?

Znaki drogowe F-10 (kierunki na pasach ruchu) w postaci opisowej. Na szczęście nie spotkasz go choćby na polskich drogach.

Może choćby udało Ci się to w kilka sekund, oczywiście o kilka sekund za długo — bo nie jesteśmy w pędzącym aucie. Jak wyglądałby znak F-8 informujący o wyznaczonym objeździe w formie tekstowej już możesz domyślić się sam.

Show me the code!

No dobra… ale co to ma wspólnego z naszym kodem? Niestety, bardzo dużo. Co zrobisz, jeżeli powiem Ci, iż tak właśnie wyglądają pisane przez nas testy? Właśnie te, które psują całą euforia z implementacji. Te, których najlepiej — gdyby w ogóle nie było, bo i tak, przy pierwszej większej zmianie przestaną się kompilować.

Wyobraź sobie, iż pracujesz nad programem, który mając na wejściu kierunek jazdy i znak drogowy potrafi stwierdzić czy zostały złamane przepisy drogowe. Z pewnością moduł tego typu musi znajdować się w oprogramowaniu autonomicznych samochodów. Na tapetę weźmiemy od razu wałkowany w tym wpisie znak F-10.

Jako doświadczony programista wiesz, iż implementację zaczyna się od testów. Dodatkowo w tym przykładowym projekcie zdecydowano się na zastosowanie narzędzi z arsenału BDD. Przepisy drogowe są często dość zawiłe, więc dobrze mieć testy, które zrozumieją też eksperci domenowi. I tak oto powstał nasz pierwszy scenariusz testowy dla znaku F-10. Który z przykładów prezentowanego znaku to Ci bardziej przypomina? Rzeczywisty — spotykany na drogach, czy może naszą wymyśloną wersję, która zawierała sam tekst?

#language: pl Zakładając, iż na drodze znajduje się znak F-10 I pozwala na jazdę 4 pasami I pozwala na skręt w lewo z lewego skrajnego pasa I pozwala na jazdą na wprost z lewego skrajnego pasa I pozwala na jazdę na wprost z drugiego pasa od lewej I pozwala na jazdę na wprost z drugiego pasa od prawej I pozwala na skręt w prawo z prawego skrajnego pasa Kiedy samochód jedzie na wprost z prawego skrajnego pasa Wtedy samochód porusza się niezgodnie z przepisami.

Dochodzimy do odwiecznego problemu, który wiele metodyk, a szczególnie właśnie BDD i DDD próbują załagodzić. Stało się dokładnie to, czego próbowaliśmy uniknąć — znów tłumaczymy pojęcia domenowe z biznesowego na programistyczny. Chociaż wszystko mieści się w zakresie języka polskiego.

Jak myślisz? Czy ekspert domenowy znający wszystkie znaki na pamięć odpowie nam, iż samochód jedzie w tym przypadku (opisanym w języku Gherkin) zgodnie ze znakiem F-10. Czy nasza sekcja założeń, na pewno opisuje dokładnie to, co powinna? Niestety możemy dojść do momentu, kiedy wszyscy będą czytać to samo, ale myśleć co innego.

Tym bardziej iż tacy eksperci niestety mają wiele innych zajęć i nie czekają tylko, aż programista zasypie ich stronami przypadków testowych. Przerwa na kawę, krótkie spotkanie na korytarzu, czy możliwość zadania szybkiego pytania na callu mogą być jedyną okazją do zweryfikowania działania naszego programu. Rób BDD — mówili… eksperci Cię zrozumieją — mówili…
Jak więc jednak zapewnić, aby ekspert domenowy nas zrozumiał. Jak pokazuje, krótka historyjka powyżej — pomóc nam może przedstawienie zrozumienia w postaci ilustracji. Nawet jeżeli nie rozumiemy dialektu naszego rozmówcy, to przecież właśnie rysunki czy pokazywanie „na migi” jest wtedy niezastąpioną formą komunikacji. Nie oszukujmy się… jako developer znasz się na programowaniu, a nie na każdej dziedzinie, w jakiej Twoje oprogramowanie będzie funkcjonować. Musisz poczuć się jak przybysz na obcej planecie i znaleźć wspólny język z jej mieszkańcami. Takie podejście może być naprawdę zbawienne dla Ciebie i dla Twojego projektu, a szczególnie dla klienta. Zrozumienie modelowanych przez Ciebie procesów i odwzorowanie ich w kodzie tak jak rozumieją je ludzie biznesu - to największe wyzwanie projektów programistycznych i ich najczęstsza przyczyna porażki.

https://medium.com/@alexdimango/building-shared-understanding-in-five-minutes-6c9e29906215

Programistyczne malowidła

W pracy mojej żony pokój programistów nazywało się tzw. jamą. Jak to mogło wyglądać? Pełny pokój nieogolonych mężczyzn i ściany wyklejone samoprzylepnymi karteczkami. Jak tak teraz o tym pomyślę, to przywołane porównanie nie wydaje się aż tak nietrafione. W końcu mamy choćby malowidła niczym u jaskiniowców :) Ale to te właśnie malowidła, odpowiednio użyte mogą sprawić, że w końcu zbudujemy wspólne zrozumienie (ang. shared understanding) wraz z biznesem.

Brałem kiedyś udział w warsztatach “Crunching ‘real-life stories’ with DDD EventStorming and combining it with BDD”, które prowadził Kenny Bass-Schwegler (znany praktyk DDD, inicjator Domain-Driven Design Heuristics) w ramach spotkania grupy DDD-WRO (zachęcam do dołączenia). Ich główną częścią było stosowanie Event Stormingu do odkrywania złożoności domeny. W tym przypadku rozpatrywaliśmy zakup biletów do kina. Brzmi banalnie, ale pod uwagę było trzeba brać takie kwestie jak rezerwacje i płatności, sale kino, projekcje, repertuar itp.

Chociaż każdy z nas chodzi do kina i domena jest powszechnie znana, to niektóre odkryte procesy wymagały znacznego doprecyzowania. Mnogość reguł biznesowych poprowadziła nas w stronę Example Mappingu. W telegraficznym skrócie Example Mapping pozwala doprecyzować odkryte pojęcia biznesowe poprzez opracowanie konkretnych przykłady zastosowań - scenariuszy. Pozwala nam to odkryć kolejne, wcześniej przeoczone reguły biznesowe i dogłębnie zrozumieć działanie modelowanej rzeczywistości. Takie zgromadzone przykłady mogą być świetnym punktem wyjścia w kierunku Process Level Event Stormingu.

Jednakże choćby tradycyjny Example Mapping przestaje czasem wystarczać. Ciężko jest się zrozumieć, kiedy poprawne opisanie konceptu biznesowego nie mieści się w kilku słowach. Zanim choćby jeszcze powstały współczesne języki mówione, ludzie posługiwali się ilustracjami. Jedziesz do obcego kraju i nie znasz czyjegoś języka? Też możesz się dogadać poprzez rysunki, i nie trzeba być do tego znanym artystą :)

Na tym prostym triku skupia się wiele współczesnych technik zapamiętywania — nie zapisuj tylko rysuj. Bullet Journal / mapy myśli itd. To wszystko przesuwa nas od słów, w stronę obrazów. Zastosowanie ilustracji w Example Mappingu jest jedną ze znanych DDD Heuristics - Example Mapping: Visualise Examples.

Rock art in Chauvet Cave in France. Photo: French Ministry of Culture and Communication

W trakcie wspomnianego szkolenia ilustracje niesamowicie rozjaśniły nam pojęcie, jakie pojawiło się w trakcie analizy domeny kina. Jedna z reguł mówiła, iż w trakcie rezerwacji miejsc na sali kinowej wybrane mogą zostać jedynie „miejsca sąsiadujące” (ang. adjacent seats). Wszystko zdawało się jasne, dopóki nie zaczęliśmy modelować procesu wyboru miejsca. Pytania można było sypać jak z rękawa:

  • Co to znaczy miejsca są sąsiadujące?
  • Czy dwa miejsca za sobą w dwóch różnych rzędach też się łapią?
  • Co, jeżeli między miejscami jest przejście?
  • Co z rezerwacją, jeżeli już takich miejsc nie ma? Pierwszą próbą było utworzenie słownika i spisanie definicji takich ważnych pojęć jak to. Niestety barierą nie do pokonania okazała się już choćby wielkość karteczek, jakie mieliśmy pod ręką… Niemożliwość zmieszczenia zrozumiałych opisów w ich ramach wskazywała na to, że łatwiej będzie posłużyć się wspomnianą wcześniej heurystyką i wykonać ilustracje.

Rezerwacja miejsc dla dwóch osób (niektóre przypadki). Możliwe jest jedynie rezerwowanie miejsc sąsiednich.

Rysunki obrazujące rozłożenie miejsc i te wybrane do rezerwacji powstały w kilka minut. Czego nie można powiedzieć o dokładnej, słownikowej definicji miejsc sąsiadujących.

Po takim przedstawieniu ekspert domenowy może jedynie zerknąć na wykonany szkic i powiedzieć czy przedstawiamy to poprawnie. Wskazanie ewentualnych brakujących przykładów też nie rodzi większych problemów. Przy zastosowaniu długiego tekstu coś mogłoby umknąć, zostać przemilczane, albo źle zinterpretowane. No więc w końcu zbudowaliśmy wspólne zrozumienie z biznesem i możemy usiąść do naszej ukochanej klawiatury. Brakuje tylko skandującego tłumu: „Idź na całość”! A tutaj jednak… ZONK!

Już po chwili w naszej głowie kotłuje się znowu mnóstwo pytań.

  • Jak się teraz upewnić, iż nasz program „będzie jak z obrazka”?
  • Jak nie duplikować w kodzie wiedzy, którą mamy na ilustracjach?
  • Gdzie w naszej „jamie” będzie dobre miejsce na powieszenie tych ilustracji?
  • A może trzeba to utrwalić w formie jakiegoś dokumentu?
  • Czy dalibyśmy radę uczynić nasz kod jedynym źródłem prawny?

Dla łatwego zrozumienia czym jest i jak może nam w tym pomóc Domain-Specific Language, posłużymy się domeną szachów. Wybór tej przykładowej domeny jest prosty. Zasady tej gry są ogólnie znane i nie ma co do nich wątpliwości. Czy wiedziałeś, iż w niektórych krajach kino od razu proponuje miejsca do rezerwacji, a później jest możliwa tylko ich ewentualna zmiana za opłatą? Ja też nie, ale Kenny nas o tym uświadomił :)

Zrozumiała domena, niezrozumiałe testy

Spójrzmy na implementację testu dotyczącą ruchu pionka, kiedy możliwe jest zbicie przeciwnika. Poświęć na jego przejrzenie dosłownie chwilkę.

it('can move diagonally if there is a piece to capture', () => { const startingPawnSquare = Square.at('B', 2); const pawnSquareToMove = Square.at('B', 3); const startingBoard = staringChessBoard(); const pawnOnBoard = startingBoard.getPiece(startingPawnSquare); const boardAfterMove = startingBoard.movePiece( pawnOnBoard, startingPawnSquare, pawnSquareToMove ); const moves = pawnOnBoard.getAvailableMoves(boardAfterMove, pawnSquareToMove); expect(moves).to.have.deep.members([ PieceMove.attackAt(Square.at('A', 6)), PieceMove.attackAt(Square.at('C', 6)), ]); });

Tak szczerze to przyznam Ci się do czegoś. Ja sam pisałem ten test, a po kilku tygodniach niestety nie rozumiem go wcale, a szczególnie jakie są warunki początkowe. Czy po jego przeczytaniu w Twojej głowie od razu pojawia się szachownica z opisanym ustawieniem? Śmiem wątpić… Zdaje się, iż ustawienie na szachownicy i możliwe ruchy — to jest właśnie sposób, w jaki możecie zbudować wspólne zrozumienie z kolegą grającym codziennie w szachy. Takie fragmenty kodu jak przedstawione powyżej, dla osoby znającej się na szachach, będą zupełnie czymś innym, niż ukochana gra. Więc zastosujmy to rozumowanie i skorzystajmy tutaj z języka domenowego (DSL). A dokładniej mówiąc — odwzorujmy go w kodzie. Ty nie koniecznie znasz wszystkie oznaczenia notacji algebraicznej, a on nigdy w życiu nie programował. Mimo to, dacie radę się dogadać. Pomoże nam w tym Domain-Specific Language realizujący ilustrowany Example Mapping. Powiem więcej! Poprzez metodę sławnego Copy-Pasta choćby szachista bez problemu zaimplementuje przypadek testowy.

Domain-Specific Language na ratunek

Wykonamy DSL dla domeny gry w szachy.

Akronim DSL oznacza język domenowy (ang. Domain-Specific Language). Parafrazując Wikipedię - zawiera on konstrukcje, które są przystosowane do rozwiązywania i reprezentacji problemów z określonej domeny. Może zostać zaimplementowany na bazie języków programowania ogólnego zastosowania. Np. język dziedzinowy, używany przez skrypty Gradle ma implementacje w Groovym i Kotlinie. Składnia delikatnie się różni, ale wyrażenia pozostają takie same.

Nasz szachowy język domenowy, wykonamy, korzystając z konsturkcji języka TypeScript i wzorca method chaining (Fluent API). Co więcej, dla lepszego efektu użyjemy popularnych Emojis, zamiast algebraicznej notacji szachowej. Dzięki temu, pisząc zwykłe testy jednostkowe, poczujemy się, jakbyśmy wykonywali ilustracje szachownicy jak na Example Mappingu.

Analizując domenę gry w szachy, można dojść do poniższego wniosku. Ruch każdej z bierek (bierka to pionek lub figura), można by opisać przykładami, których schemat wygląda następująco:

  • GIVEN: określone ustawienie bierek na szachownicy.
  • WHEN: zostanie wybrana bierka do wykonania ruchu.
  • THEN: wybrana bierka może ruszyć się i zaatakować odpowiednie dla niej pola.

Zobaczmy, jak taki test może wyglądać, po zaimplementowaniu przeznaczonego dla szachów DSL.

pawn.spec.ts

EmojiChessBoardTestCase.givenChessBoard({ 8: ['♜', '♞', '♝', '♛', '♚', '♝', '♞', '♜'], 7: [' ', '♟', ' ', '♟', '♟', '♟', ' ', '♟'], 6: [' ', ' ', ' ', ' ', '♙', ' ', ' ', ' '], 5: [' ', ' ', ' ', '♙', ' ', ' ', ' ', ' '], 4: ['♟', ' ', '♟', ' ', ' ', ' ', ' ', ' '], 3: [' ', '♙', ' ', ' ', ' ', ' ', '♟', ' '], 2: ['♙', ' ', '♙', ' ', ' ', '♙', '♙', '♙'], 1: ['♖', '♘', '♗', '♕', '♔', '♗', '♘', '♖'], 0: ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'], }) .whenPick({ piece: '♙', on: 'B3' }) .thenNormalMoves(['B4']) .thenAttackMoves(['A4', 'C4']) .asTest('can move diagonally if there is a piece to capture');

Raport ze spełnionego testu pokazywany w konsoli naszego CI.

Czy jest to aż tak wielka rewolucja? Czy wreszcie zbudowaliśmy wspólne zrozumienie? Myślę, iż to na pewno krok naprzód, ale można jeszcze lepiej… Wszystko wygląda dobrze — użyliśmy emoji w sekcji GIVEN, która stała się teraz niesamowicie czytelna. Niestety ekspert domenowy dalej musi poświęcić chwilkę skupienia i wczytać się w nasze asercje. W tym przypadku nie pozostało aż tak źle. Zobaczmy co gdybyśmy testowali ruchy królowej / hetmana.

queen.spec.ts

EmojiChessBoardTestCase.givenChessBoard({ 8: [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '], 7: [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '], 6: [' ', ' ', ' ', '♟', ' ', ' ', ' ', ' '], 5: [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '], 4: ['♟', '♟', ' ', '♕', ' ', ' ', ' ', '♟'], 3: [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '], 2: [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '], 1: [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '], 0: ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'], }) .whenPick({ piece: '♕', on: 'D4' }) .thenNormalMoves([ 'C4', 'E4', 'F4', 'G4', 'D1', 'D2', 'D3', 'D5', 'A1', 'B2', 'C3', 'E5', 'F6', 'G7', 'H8', 'A7', 'B6', 'C5', 'E3', 'F2', 'G1', ]) .thenAttackMoves(['B4', 'D6', 'H4']) .asTest('should not skip (jump over) enemy chess');

Jak gwałtownie zweryfikować czy

[ 'C4', 'E4', 'F4', 'G4', 'D1', 'D2', 'D3', 'D5', 'A1', 'B2', 'C3', 'E5', 'F6', 'G7', 'H8', 'A7', 'B6', 'C5', 'E3', 'F2', 'G1', ];

to poprawne ruchy hetmana w przedstawionej sytuacji? O ile dodanie nowych testów i ustawianie początkowych kombinacji na szachownicy jest proste, o tyle ciężko już odczytać czy asercja została poprawnie sformułowana. Pójdźmy więc jeszcze o krok dalej i zilustrujmy też nasze oczekiwania w części THEN. Wykorzystamy do tego analogiczny mechanizm jak dla warunków początkowych.

Dzięki dobraniu odpowiednich oznaczeń także nasze oczekiwania możemy wyrazić w sposób zrozumiały na pierwszy rzut oka.

queen.spec.ts

EmojiChessBoardTestCase.givenChessBoard({ 8: [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '], 7: [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '], 6: [' ', ' ', ' ', '♟', ' ', ' ', ' ', ' '], 5: [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '], 4: ['♟', '♟', ' ', '♕', ' ', ' ', ' ', '♟'], 3: [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '], 2: [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '], 1: [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '], 0: ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'], }) .thenExpectMoves({ 8: [' ', ' ', ' ', ' ', ' ', ' ', ' ', '👟'], 7: ['👟', ' ', ' ', ' ', ' ', ' ', '👟', ' '], 6: [' ', '👟', ' ', '🗡️', ' ', '👟', ' ', ' '], 5: [' ', ' ', '👟', '👟', '👟', ' ', ' ', ' '], 4: [' ', '🗡️', '👟', '♕', '👟', '👟', '👟', '🗡️'], 3: [' ', ' ', '👟', '👟', '👟', ' ', ' ', ' '], 2: [' ', '👟', ' ', '👟', ' ', '👟', ' ', ' '], 1: ['👟', ' ', ' ', '👟', ' ', ' ', '👟', ' '], 0: ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'], }) .asTest('should not skip (jump over) enemy chess');

Raport CHAI z testów.

Dzięki przedstawieniu oczekiwanego rezultatu dzięki naszego DSLa otrzymujemy bardzo czytelny raport z testów. Jesteśmy błyskawicznie w stanie zweryfikować czy test poprawnie definiuje ruchy hetmana.
Cały kod źródłowy przedstawionej implementacji, jak zawsze, na moim GitHubie w repozytorium DDD.EventSourcing.PortsAndAdapters.TypeScript.NestJS.Chess.

Kiedy DSL się opłaca

Powiem Ci więcej! Nie tylko zrozumienie testów jest szybkie, ale i ich dopisywanie. Bez takiego DSL choćby nikt z zespołu, w którym realizowałem ten projekt w trakcie kursu CodersCamp, nie chciał pisać testów. Jednakże przejście na odpowiednio przystosowany do naszych przypadków DSL skutkuje pojawianiem się nowych testów jak grzyby po deszczu. Łatwa konstrukcja i świadomość tego, iż nie przestaną się zaraz kompilować, skutecznie zachęca do dopisywania nowych przypadków. Na pewno jest to przyjemniejsze i szybsze od „przeklikiwania” aplikacji. Łatwiej niż ponarzekać „Gdy tylko zmienię coś w kodzie, to testy przestają się kompilować”, jest dopisać nowy przypadek i mieć spokojny wieczór, wiedząc, iż kod działa, jak powinien.

Czy w Twoim projekcie są testy ciężkie do utrzymania, a każdy nowy wymaga powtarzania tego samego schematu i zmiany kilku wartości? Jeśli tak, to warto ukryć tę duplikację pod warstwą DSL. Kod testowy, aby być czytelny, powinien rządzić się takimi samymi prawami jak ten idący na produkcję.

Rozważając za i przeciw pamiętaj, iż każdy DSL to dodatkowa warstwa abstrakcji nad testowanym zachowaniem systemu. Ciężej będzie ogarniać co dzieje się pod spodem i zmiany samego DSLa mogą wymagać zaawansowanej wiedzy. Jednak zrozumienie działania „under the hood” może nie być konieczne dla wszystkich z zespołu. A jeżeli Twój DSL będzie odpowiednio stabilny, możesz spodziewać się błyskawicznego przyrostu liczby testów. Nawet jeżeli coś zmieni się w wywoływanym przez DSL kodzie, to na szczęście sama warstwa języka dziedzinowego i zdefiniowane, za jego pomocą przypadki testowe, powinny pozostać niezmienione — w myśli zasady Open-Closed.

W przedstawionym przykładzie szachów, jeżeli cała implementacja „pod spodem” zmieniłaby się, to ciała metod opisujących przypadki testowe nie wymagałby refaktoryzacji, dzięki osiągniętej enkapsulacji. Ponieważ posługujemy się właśnie językiem domenowym, który jest niezależny od implementacji. Przecież nie ważne co napiszemy w naszym kodzie, to nie skutkuje to zmianą zasad gier w szachy, czy przepisów ruchu drogowego. Jedynie same konstrukcje parsujące naszego DSLa musiałby zostać dostosowane i wywoływać operacje na nowym modelu.

Powrót do początku

A skoro o przepisach drogowych mowa… Czy zauważyłeś, kiedy przeszliśmy z tematu autonomicznych pojazdów do gry w szachy? Nasz pierwotny wątek został trochę „zagłodzony”. Więc czas jeszcze na chwilę do niego powrócić. Bo przykładów przecież nigdy dojść :)

Językiem, który jak dla mnie posiada najbardziej przyjazną syntaktykę — sprzyjającą tworzeniu czytelnych DSL jest Kotlin. Dlatego też poniżej przedstawiam Ci przykład w Kotlinie, dla opisywanego wcześniej przypadku ze znakiem F-10 (pamiętasz już, czego on dotyczy, no nie?).

Znaki drogowy F-10, przetestowany poprzez poniższy DSL.

@Test fun `Scenario | Drive straight on lane for turn right`() { onRoad { sign(F10("⬅⬆", "⬆", "⬆", "➡")) whenDrive(direction = "⬆", onLane = 4) thenRoadRulesBroken() } } @Test fun `Scenario | Drive straight on lane for straight and turn left`() { onRoad { sign(F10("⬅⬆", "⬆", "⬆", "➡")) whenDrive(direction = "⬆", onLane = 1) thenRoadRulesPreserved() } } @Test fun `Scenario | Drive right on lane for straight and turn left`() { onRoad { sign(F10("⬅⬆", "⬆", "⬆", "➡")) whenDrive(direction = "➡", onLane = 1) thenRoadRulesBroken() } }

Całą implementację, znajdziesz jak zwykle na moim GitHubie.

Co myślisz o DSLach po przeczytaniu tego wpisu? A może użycie Emojis (albo kodów ASCII jak w ostatnim przykładzie) zainspirowały Cię do jakiś programistycznych ilustracji konceptów domenowych? Czy używałeś już kiedyś DSL dla potrzeb testów albo w swoim kodzie produkcyjnym? A może całkowicie nie zgadzasz się ze stanowiskiem przedstawionym w tym wpisie :) ? Mam nadzieję, iż chociaż dzięki niemu, zapamiętałeś już, czym jest znak F-10 :) Zawsze jestem po drugiej stronie monitora i z chęcią odpowiem na Twoje komentarze albo podyskutuję na portalach społecznościowych.

Gdzie dalej potoczy się życie na kodach

Pokazane tutaj przykłady były bardzo proste i specyficzne dla analizowanych domen (stąd w końcu nazwa Domain-Specific Language). Zobaczyłeś też jak używać DSL i jakie są tego benefity. Zapewne chciałbyś też wiedzieć, w jaki sposób to wszystko działa i jak możesz zaimplementować DSL w swoim projekcie? W kolejnym wpisie na ten temat wykonamy bardziej zaawansowany i rozbudowany DSL wraz z dokładnym opisem implementacji. Tym razem naszą „domeną” będą building blocki DDD. Temat na tyle generyczny, iż jeżeli stosujesz Domain-Driven Design, to przedstawione rozwiązania będą dla Ciebie wręcz „ready-to-go”. M.in. zobaczysz jak poprzez DSL testować model agregatu emitującego zdarzenia domenowego. Stay tuned!

Tak na narobienie Ci smaku, mały kod w Kotlinie na przykładzie rezerwacji miejsca w autobusie :)

givenAggregate { seat } withPriorEvents { seatReserved } whenCommand { reserveSeat } thenExpectEvent { seatReservationFailed }

Inni też tym żyją…

Jeśli chcesz zagłębić się w tematy poruszane w tym wpisie, to polecam Ci na początek te miejsca w Internetach.

  • Michał Bartyzel | Przykład jedynym źródłem prawdy W tym wpisie Michał opisuje sytuację remontu w swoim domu, gdzie brak przykładu doprowadził do totalnego nieporozumienia z ekipą wykonawczą.

  • GitHub ddd-by-examples | Efekt zastosowania Example Mappingu na domenie biblioteki możesz zobaczyć np. w świetnym repozytorium github.com/ddd-by-examples/library, a dokładnie tutaj: Example Mapping i Design Level Event Storming. Dodatkowo autorzy także pokazali przykład zastosowania DSL w testach jednostkowych. Wszystko zostało ładnie wytłumaczone w README repozytorium. Enjoy!

    def 'should check out book which is marked as placed on hold in the system'() { given: BookDSL bookOnHold = aCirculatingBook() with anyBookId() locatedIn anyBranch() placedOnHoldBy anyPatron() and: LibraryBranchId aBranch = anyBranch() and: BookCheckedOut bookCheckedOutEvent = the bookOnHold isCheckedOutBy anyPatron() at aBranch when: CheckedOutBook checkedOutBook = the bookOnHold reactsTo bookCheckedOutEvent then: checkedOutBook.bookId == bookOnHold.bookId checkedOutBook.checkedOutAt == aBranch checkedOutBook.version == bookOnHold.version }
  • YouTube - Anton Arhipov | Kotlin DSL in under an hour

  • Kotlin Docs | Type-Safe Builders Kotlin-based domain-specific languages (DSLs)

  • JetBrains | What are Domain Specific Languages (DSLs)?

Idź do oryginalnego materiału