TDD: Drugi przykład – Lista zadań. Cz 2.

Posted by Przemysław Owsianik on 2021-10-03

Jako, że kontynuujemy pisanie naszej aplikacji, to przypomnijmy sobie listę rzeczy już zrobionych (w tej chwili jedna pozycja ;)) i tych, które jeszcze pozostały do zrobienia:

  • Zadania mogę tworzyć dla maksymalnie 30 dni (dziś + 29).
  • Zapisywanie zadania do bazy-gdy wszystko idzie dobrze.
  • Zapisywanie zadania do bazy – baza rzuca wyjątek podczas zapisu
  • Mogę wyświetlić zadania dla danego dnia.
  • Mogę oznaczyć zadanie jako wykonane (innymi słowy mogę je modyfikować)

Kończąc ostatnią część zdecydowałem, że kolejną rzeczą za którą się zabierzemy będzie obsługa sytuacji wyjątkowych. Dlaczego? Zazwyczaj wybierając kolejny obszar do wykonania, staram się dosyć szybko zrobić kilka różnych zagadnień żeby nadać projektowi mniej lub bardziej określony kształt. Nie mniej w przykładzie należy kuć żelazo póki gorące ;). W tym wypadku chciałem podkreślić, że obok wspomnianej w poprzedniej części happy path, jest też sad path, która zbyt często jest pomijana. Ta pierwsza jest w naszym wypadku wówczas gdy zadanie bezproblemowo zapisuje się do bazy. Druga, gdy podczas zapisu coś idzie nie tak. Wypadało by się więc zastanowić co powinno się stać gdy nastąpi problem po stronie bazy. Na tą chwilę myślę, że wystarczającym działaniem będzie zalogowanie tej informacji. Napiszmy więc, w klasie DbTaskSaverTests, nowy test. Doprowadzam też do możliwości skompilowania kodu.

---

!!! W tym poście nie będę już podawał całych listingów, a tylko fragmenty. Postaram się żeby byly jak najbardziej dokładne i obrazowe ale jeżeli ktoś chce prześledzić cały kod, to zapraszam na mojego github’a do repo WhatToDo – commity są robione po każdym z cykli R-G-R.

---

Test wygląda następująco:

[Fact]
 public void SaveTask_WhenIDatabaseThrowException_LogInfoAboutError()
 {
     this.mockDatabase.Setup(db => db.Write(It.IsAny())).Throws(new Exception());

     Task taskToSave = new Task();
     this.sut.SaveTask(taskToSave);

     this.mockLogger.Verify(l=>l.LogException(It.IsAny()), Times.Once());
 }

Jak (mam nadzieję :)) widać, utworzony został nowy mock dla loggera, ILogger to interfejs, którego implementacja jest wstrzykiwana do DbTaskSaver, poprzez konstruktor. Aby umożliwić kompilatorowi pracę, tworzę wspomniany interfejs. Następnie dokonuję zmian sprawiających, że test przechodzi. Cały czas jednak wstrzymuje się z utworzeniem klasy implementującej ILogger. Zamiast tego wykreślam z naszej listy pozycję z bazą rzucającą wyjątek i dodaję nową :”Mogę logować błędy do pliku”.  Dlaczego do pliku? A dlaczego nie? 😉 Teraz zastanowię się czym zajmę się dalej…Gdy spojrzymy na listę, to zobaczymy, że temat tworzenia zadań nie został wyczerpany. Myślę, że nic złego się nie stanie jeżeli zabierzemy się za pierwszą pozycję na liście:

  • Zadania mogę tworzyć dla maksymalnie 30 dni (dziś + 29).
  • Zapisywanie zadania do bazy-gdy wszystko idzie dobrze.
  • Zapisywanie zadania do bazy – baza rzuca wyjątek podczas zapisu
  • Mogę wyświetlić zadania dla danego dnia.
  • Mogę oznaczyć zadanie jako wykonane (innymi słowy mogę je modyfikować)
  • Mogę logować błędy do pliku

Gdybyśmy nie usiłowali pisać wedle prawideł TDD, to moglibyśmy się zacząć zastanawiać w tym miejscu jaka klasa powinna być odpowiedzialna za sprawdzenie czy tworzone zadanie spełnia podaną zasadę. My jednak stoimy na głowie i w tej chwili nie musimy się takimi kwestiami przejmować :). Za to jest odpowiedzialny krok trzeci cyklu (Red-Green-Refactor oczywiście), a i to wątpliwe, że znajdziemy odpowiedź podczas najbliższej iteracji. W tej chwili miejscem na sprawdzenie tego warunku jest klasa zapisująca do bazy (zaufajcie mi :)). Piszę więc w DbTaskSaverTests kolejny przypadek. Dla uproszczenia, będziemy chcieli aby w przypadku próby zapisania zadania z datą większą niż 29 dni od chwili obecnej logowana była informacja o tym:

[Fact]
[Fact]
 public void SaveTask_WhenTaskDateIsLargerThenThirtyDays_LogInfo()
 {
     Task taskToSave = new Task();
     taskToSave.Date = DateTime.Today + new TimeSpan(30, 0, 0, 0);

     this.sut.SaveTask(taskToSave);

     this.mockLogger.Verify(l => l.LogException(It.IsAny()), Times.Once());
 }

Aby powyższy test przeszedł należy dokonać oczywiście zmian w metodzie SaveTask. Staramy się to zrobić w jak najszybszy sposób – tak aby test zaczął przechodzić. Możemy zrobić coś takiego:

public void SaveTask(Task taskToSave)
 {
     if (taskToSave.Date > DateTime.Today + new TimeSpan(29, 0, 0, 0))
     {
         this.logger.LogException("Próba zapisania zadania na zbyt odległą datę.");
         return;
     }

     try
     {
         this.database.Write(taskToSave);
     }
     catch (Exception ex)
     {
         this.logger.LogException(ex.Message);
     }
 }

I znów przypominam: w tej chwili jakość tego kodu nie jest najważniejsza – dlatego bo to co napisaliśmy nie jest wersją ostateczną. Moglibyśmy pokusić już o refaktoring, ale według mnie możemy pozwolić sobie na jeszcze chwilę zwłoki. Na razie możemy nasze wątpliwości (w moim wypadku jest to kwestia sposobu i umiejscowienia obsługi błędów przy zapisywaniu zadania) dopisać do naszej listy. Jednocześnie zastanawiam się nad kolejną kwestią do poruszenia. Szukam symetrycznych operacji – mamy wyświetlanie zadań na dany dzień, jednak żeby wyświetlić zadania musimy odczytać je z bazy. Na tę chwilę nasza lista wygląda więc tak:

  • Zadania mogę tworzyć dla maksymalnie 30 dni (dziś + 29).
  • Zapisywanie zadania do bazy-gdy wszystko idzie dobrze.
  • Zapisywanie zadania do bazy – baza rzuca wyjątek podczas zapisu
  • Mogę wyświetlić zadania dla danego dnia.
  • Mogę odczytać z bazy, istniejące w niej zadania (na dany dzień)
  • Mogę oznaczyć zadanie jako wykonane (innymi słowy mogę je modyfikować)
  • Mogę logować błędy do pliku
  • Muszę się zastanowić nad obsługą błędów przy zapisywaniu zadań – do refaktoryzacji.

Kolejny test napiszemy dla nowej klasy. Jeżeli mamy DbTaskSaver to przydałby się również DbTaskLoader 🙂 – byłby on oczywiście odpowiedzialny za odczytywanie zadań z bazy. Najpierw napiszmy jednak test. Test musi on być napisany w nowej klasie testowej  (wydaje mi się, że jest to oczywiste, więc nie będę przytaczał całego kodu klasy):

[Fact]
 public void LoadTaskForDay_OnceCallWrite_FromIDatabaseImpl()
 {
     this.sut.LoadTaskForDay(new DateTime());

     this.mockDatabase.Verify(db => db.Load(It.IsAny()), Times.Once());
 }

Aby kod się kompilował, tworzymy klasę DbTaskLoader zawierającą metodę LoadTaskForDay. Musimy też zrobić zmiany w naszej pseudo-bazie, czyli klasie implementującej IDatabase. Następnie doprowadzamy do przejścia testu, analogicznie jak robiliśmy to w DbTaskSaver. Test przechodzi. Myślę, że to nam pobieżnie załatwia temat odczytywania danych z bazy. Oznaczmy więc progres na naszej liście i zastanówmy się nad kolejną kwestią:

  • Zadania mogę tworzyć dla maksymalnie 30 dni (dziś + 29).
  • Zapisywanie zadania do bazy-gdy wszystko idzie dobrze.
  • Zapisywanie zadania do bazy – baza rzuca wyjątek podczas zapisu
  • Mogę wyświetlić zadania dla danego dnia.
  • Mogę odczytać z bazy, istniejące w niej zadania (na dany dzień)
  • Mogę oznaczyć zadanie jako wykonane (innymi słowy mogę je modyfikować)
  • Mogę logować błędy do pliku
  • Muszę się zastanowić nad obsługą błędów przy zapisywaniu zadań – do refaktoryzacji.

Wyświetlanie zadań na dany dzień kojarzy mi się z jakimś GUI :). Mając GUI, możemy efekty naszej pracy pokazać „klientowi”.  Ale to już w przyszłej części – przyjrzymy się też w końcu refaktoryzacji w ujęciu TDD.