Test Driven Development z użyciem JUnit 5. Część 4

Czwarta część naszego artykułu na temat Test Driven Development z JUnit 5. Tym razem kontynuujemy refaktoryzację aplikacji do zarządzania lotami.

wrz 10, 2021 60

4. Refaktoryzacja aplikacji do zarządzania lotami


Aby przenieść aplikację do zarządzania lotami do TDD, musimy najpierw pokryć istniejącą logikę biznesową testami JUnit 5. Dodajemy zależności JUnit 5 (junit-jupiter-api i junit-jupiter-engine) do pliku pom.xml Mavena.

Chcielibyśmy dokonać refaktoryzacji i zastąpić instrukcje warunkowe polimorfizmem.

Kluczem do refaktoryzacji jest przeniesienie projektu do używania polimorfizmu zamiast kodu warunkowego w stylu proceduralnym. W przypadku polimorfizmu (zdolność jednego obiektu do przejścia więcej niż jednego testu IS-A) wywoływana metoda jest określana nie w czasie kompilacji, ale w czasie wykonywania, w zależności od efektywnego typu obiektu.

Zasada działająca tutaj nazywa się "zasadą otwartej / zamkniętej". W praktyce oznacza to, że projekt przedstawiony po lewej stronie będzie wymagał zmian w istniejącej klasie za każdym razem, gdy dodamy nowy typ lotu. Zmiany te mogą mieć odzwierciedlenie w każdej warunkowej decyzji podjętej w oparciu o typ lotu. Dodatkowo jesteśmy zmuszeni polegać na polu flightType i wprowadzać niewykonane przypadki domyślne.

Refactoring the flight-management application.png


Mając projekt po prawej stronie - który jest refaktoryzowany poprzez zastąpienie warunkowe z polimorfizmem - nie potrzebujemy oceny typu flightType ani wartości domyślnej w instrukcjach przełącznika. Możemy nawet dodać nowy typ - przewidujmy trochę i nazwijmy go PremiumFlight - po prostu rozszerzając klasę bazową i definiując jej zachowanie. Zgodnie z zasadą open / closed hierarchia będzie otwarta na rozszerzenia (możemy łatwo dodawać nowe klasy) ale zamknięta na modyfikacje (istniejące klasy, począwszy od klasy bazowej Flight, nie zostaną zmodyfikowane).

Skąd możemy mieć pewność, że postępujemy właściwie i nie wpływamy na już działającą funkcjonalność? Odpowiedź jest taka, że zdanie testów daje pewność, że istniejąca funkcjonalność pozostanie nienaruszona. Korzyści z podejścia TDD naprawdę się ujawniają!

Refaktoryzacja zostanie osiągnięta poprzez zachowanie podstawowej klasy lotu i, dla każdego typu warunkowego, dodanie oddzielnej klasy w celu przedłużenia lotu. Zmienimy addPassenger i removePassenger na metody abstrakcyjne i delegujemy ich implementację do podklas. Pole flightType nie ma już znaczenia i zostanie usunięte

public abstract class Flight { #1
private String id;
List passengers = new ArrayList(); #2
public Flight(String id) {
this.id = id;
}
public String getId() {
return id;
}
public List getPassengersList() {
return Collections.unmodifiableList(passengers);
}
public abstract boolean addPassenger(Passenger passenger); #3
public abstract boolean removePassenger(Passenger passenger); #3
}

W tym listingu:

  • Deklarujemy klasę jako abstrakcyjną, czyniąc ją podstawą hierarchii lotów # 1.
  • Sprawiamy, że lista pasażerów jest prywatna, umożliwiając bezpośrednie dziedziczenie przez podklasy w tym samym pakiecie # 2.
  • Deklarujemy addPassenger i removePassenger jako metody abstrakcyjne, delegując ich implementację do podklas # 3.

Przedstawiamy klasę EconomyFlight, która rozszerza Flight i implementuje odziedziczone abstrakcyjne metody addPassenger i removePassenger.

public class EconomyFlight extends Flight { #1
public EconomyFlight(String id) { #2
super(id); #2
} #2
@Override
public boolean addPassenger(Passenger passenger) { #3
return passengers.add(passenger); #3
} #3
@Override
public boolean removePassenger(Passenger passenger) { #4
if (!passenger.isVip()) { #4
return passengers.remove(passenger); #4
} #4
return false; #4
} #4
}

W tym listingu:

  • Deklarujemy klasę EconomyFlight rozszerzającą klasę abstrakcyjną Flight # 1 i tworzymy konstruktor wywołujący konstruktora nadklasy # 2.
  • Wdrażamy metodę addPassenger zgodnie z logiką biznesową: po prostu dodajemy pasażera do lotu ekonomicznego bez ograniczeń # 3.
  • Wdrażamy metodę removePassenger zgodnie z logiką biznesową: pasażer może zostać usunięty z lotu tylko wtedy, gdy pasażer nie jest VIPem # 4.

Wprowadzamy również klasę BusinessFlight, która rozszerza Flight i implementuje odziedziczone abstrakcyjne metody addPassenger i removePassenger.

public class BusinessFlight extends Flight { #1
public BusinessFlight(String id) { #2
super(id); #2
} #2
@Override
public boolean addPassenger(Passenger passenger) { #3
if (passenger.isVip()) { #3
return passengers.add(passenger); #3
} #3
return false; #3
} #3
@Override
public boolean removePassenger(Passenger passenger) { #4
return false; #4
} #4
}

W tym listingu:

  • Deklarujemy klasę BusinessFlight rozszerzającą klasę abstrakcyjną Flight # 1 i tworzymy konstruktor wywołujący konstruktora nadklasy # 2.
  • Wdrażamy metodę addPassenger zgodnie z logiką biznesową: do lotu biznesowego nr 3 można dodać tylko pasażera VIP.
  • Wdrażamy metodę removePassenger zgodnie z logiką biznesową: pasażera nie można usunąć z lotu biznesowego nr 4.

Po refaktoryzacji poprzez zastąpienie warunku polimorfizmem natychmiast widzimy, że metody wyglądają teraz na znacznie krótsze i wyraźniejsze, nie są zaśmiecone procesem decyzyjnym. Nie jesteśmy również zmuszeni do traktowania poprzedniego przypadku domyślnego, którego nigdy nie oczekiwano i który spowodował wyjątek. Oczywiście refaktoryzacja i zmiany API są propagowane do testów, jak pokazano poniżej.

public class AirportTest {
@DisplayName("Given there is an economy flight")
@Nested
class EconomyFlightTest {
private Flight economyFlight;
@BeforeEach
void setUp() {
economyFlight = new EconomyFlight("1"); #1
}
[...]
}
@DisplayName("Given there is a business flight")
@Nested
class BusinessFlightTest {
private Flight businessFlight;
@BeforeEach
void setUp() {
businessFlight = new BusinessFlight("2"); #2
}
[...]
}
}

W tym wykazie zastępujemy poprzednie wystąpienia lotów instancjami EconomyFlight nr 1 i BusinessFlight nr 2. Usuwamy również klasę Airport, która służyła jako klient klas Pasażer i Lot - teraz, gdy wprowadziliśmy testy, nie jest już potrzebna. Wcześniej służyło to do zadeklarowania głównej metody, która stworzyła różne typy lotów i pasażerów i zmusiła ich do wspólnego działania.

Interesujesz się JUnit? Sprawdź nasze szkolenia


Catalin Tudose
Java and Web Technologies Expert

Udostępnij


Masz jeszcze jakieś pytania?
Skontaktuj się z nami
Thank you.
Your request has been received.