fbpx

Co to są Czyste Testy?

Zasady Czystego Kodu dotyczą w równym stopniu kodu produkcyjnego oraz kodu testowego. Wykonuj więc czyszczenie kodu za każdym razem, także gdy piszesz testy. Okazje do refaktoryzacji zauważysz często już zaraz po dodaniu nowego testu albo nawet jeszcze przed jego napisaniem . Będzie tak, kiedy w nowy tescie potrzeba części znajdujących się już w innych testach – jak asercje lub konfiguracja systemu.

Takie korekty powinny uwzględniać podstawowe zasady Czystego Kodu. Dotyczą one głownie zachowania czytelności jak i utrzymania łatwości wprowadzania kolejnych zmian. Dbajmy także o to, aby kod można było szybko przeczytać i zrozumieć.

Przykład do refaktoryzacji

Poniżej umieściłem zestaw kilku testów integracyjnych. Sprawdzają one cennik pobytu w klubie fitness (siłownia, sauna, basen). Logika zawiera dodatkowo wyliczenia punktów stałego klienta.

Chociaż przykład tego testu jest dosyć krótki to już zawiera kilka duplikacji kodu. Powtórzenia kodu można znaleść na początku oraz końcu każdego przypadku testowego.

@Test
public void twoHours_isOnly_payEntryFee() {
  Facility beFitGym = new Facility("Be Fit Gym", Facility.GYM);
  Visit visit = new Visit(beFitGym, 2);
  Client client = new Client("Mike");

  // when
  client.addVisit(visit);
  String payment = client.getReceipt();

  // Then
  assertThat(payment)
    .valueByXPath("/table/tr[1]/td[1]")
    .isEqualTo("Be Fit Gym");
        
  assertThat(payment)
    .valueByXPath("/table/tr[1]/td[2]")
    .isEqualTo("4.0");
        
  assertThat(payment)
    .valueByXPath("/table/tr[1]/td[3]")
    .isEqualTo("100");
}

@Test
public void twoHours_PayForEach() {
  // Given
  Facility beFitGym = new Facility("Jacuzzi", Facility.STEAM_BATH);
  Visit visit = new Visit(beFitGym, 2);
  Client client = new Client("Mike");

  // When
  client.addVisit(visit);
  String payment = client.getReceipt();

  // Then
  assertThat(payment)
   .valueByXPath("/table/tr[1]/td[1]")
   .isEqualTo("Be Fit Jacuzzi");
        
  assertThat(payment)
    .valueByXPath("/table/tr[1]/td[2]")
    .isEqualTo("10.0");

  assertThat(payment)
    .valueByXPath("/table/tr[1]/td[3]")
    .isEqualTo("300");
}

Refaktoryzacja w małych krokach

Formatowanie

Zanim dokonam pierwszej tranformacji, zauważ wartość formatowania kodu. Powyższy kod został już zformatowany. Wcześniej wyglądał jak poniżej. Zapewne widzisz róznicę kiedy kod jest czytelniejszy?

@Test
public void twoHours_PayForEach() {
  ...
  assertThat(payment).valueByXPath("/table/tr[1]/td[1]").isEqualTo("Gym");
  assertThat(payment).valueByXPath("/table/tr[1]/td[2]").isEqualTo("10.0");
  assertThat(payment).valueByXPath("/table/tr[1]/td[3]").isEqualTo("300");
}

Uzależnij asercje od zmiennych lokalnych

W kodzie dobrze sformatowanym powtórzenia kodu są bardziej widoczne . W ten sposób przygotowuję kod do wyciągnięcia metod zawierających powtórzenia logiki. Zanim wykonam ekstrakcję metody uzależnię powtarzający się kod od zmiennych lokalnych poprzez ich ekstrakcję.

@Test
public void twoHours_payEntryFee() {
  // Given
  Facility beFitGym = new Facility("Be Fit Gym", Facility.GYM);
  Visit visit = new Visit(beFitGym, 2);
  Client client = new Client("Mike");

  // When
  client.addVisit(visit);
  String payment = client.getReceipt();

  // Then
  String facilityName = "Be Fit Gym";
  String facilityPrice = "4.0";
  String facilityPoints = "100";

  assertThat(payment)
   .valueByXPath("/table/tr[1]/td[1]")
   .isEqualTo(facilityName);
        
  assertThat(payment)
    .valueByXPath("/table/tr[1]/td[2]")
    .isEqualTo(facilityPrice);

  assertThat(payment)
    .valueByXPath("/table/tr[1]/td[3]")
    .isEqualTo(facilityPoints);
}

Dokonaj ekstrakcji metody z asercjami

Teraz czas na ekstrakcję metody. Jest to automatyczna tranformacja kodu w większości środowisk programistycznych języka Java.

private void assertFacility(String payment, 
    String facilityName, 
    String facilityPrice, 
    String facilityPoints) { 
   
  assertThat(payment)
    .valueByXPath("/table/tr[1]/td[1]")
    .isEqualTo(facilityName);

  assertThat(payment)
    .valueByXPath("/table/tr[1]/td[2]")
    .isEqualTo(facilityPrice);

  assertThat(payment)
    .valueByXPath("/table/tr[1]/td[3]")
    .isEqualTo(facilityPoints);
}

Wyciągnięte zmienne lokalne nie są już potrzebne, wieć możemy dokonać ich wchłonięcia. Poniżej rezultat przekształcenia.

@Test
public void twoHours_isOnly_payEntryFee() {
  Facility beFitGym = new Facility("Be Fit Gym", Facility.GYM);
  Visit visit = new Visit(beFitGym, 2);
  Client client = new Client("Mike");

  // when
  client.addVisit(visit);
  String payment = client.getReceipt();

  // Then
  assertFacility(payment, "Be Fit Gym", 4.0, 100);
}

@Test
public void twoHours_PayForEach() {
  // Given
  Facility beFitGym = new Facility("Jacuzzi", Facility.STEAM_BATH);
  Visit visit = new Visit(beFitGym, 2);
  Client client = new Client("Mike");

  // When
  client.addVisit(visit);
  String payment = client.getReceipt();

  // Then
  assertFacility(payment, "Jacuzzi", 10.0, 150);
}

Zaracaj uwagę na parametry metod

Zauważ, że testy stały się krótsze. Problemem jednak jest teraz ilość parametrów które dodatkowo należą do dwóch grup. Pierwsza grupa to dane wejściowe (pierwszy parametr) a druga to wartości poszczególnych asercji (kolejne trzy parametry). Dodatkowo jeżeli parametry obok siębie są tego samego typu to łatwo jest się pomylić w ich kolejności.


Pobierz bezpłatne materiały z V/Blogów i dołącz do newlettera

Webinary, Artykuły oraz Kursy Online i Stacjonarne

Wysyłaj do mnie newletter (możesz się wypisać w każdej chwili).

Akceptuję Politykę Prywatności i Regulamin


Utwórz nową klasę asercji

Dalej użyję dwie powyższe grupy parametrów jako kierunku do kolejnych zmian. Umieszczam metodę w nowej klasie oraz definiuję jedną z grup jako parametr kontruktora. Wówczas obecna metoda będzie zawierać tylko parametry z drugiej grupy a dostęp do pierwszej grupy uzyska poprzez pola klasy.

Dokonaj ektrakcji klasy poprzez ekstrakcję delegata

Aby utworzyć nową klasę wykonuję ekstrakcję delegata, która jest kolejnym zautomatyzowanym przekszałceniem w IntelliJ IDE dla języka Java.

Oto rezultat tranformacji kodu.

private final FacilityAssertion facilityAssertion = new FacilityAssertion();

@Test
public void twoHours_isOnly_payEntryFee() {
  Facility beFitGym = new Facility("Be Fit Gym", Facility.GYM);
  Visit visit = new Visit(beFitGym, 2);
  Client client = new Client("Mike");

  // when
  client.addVisit(visit);
  String payment = client.getReceipt();

  // Then
  facilityAssertion.assertFacility(payment, "Be Fit Gym", 4.0, 100);
}

@Test
public void twoHours_PayForEach() {
  // Given
  Facility beFitGym = new Facility("Jacuzzi", Facility.STEAM_BATH);
  Visit visit = new Visit(beFitGym, 2);
  Client client = new Client("Mike");

  // When
  client.addVisit(visit);
  String payment = client.getReceipt();

  // Then
  facilityAssertion.assertFacility(payment, "Jacuzzi", 10.0, 150);
}

Wykonaj wchłonięcie pola

Dodatkowe pole w klasie nie było moim celem. Dokonuję więć wchłonięcia tej pola. Wówczas nowy obiekt asercji będzie utworzony od nowa w każdym miejscu gdzie pole było używane przez logikę.

@Test
public void twoHours_isOnly_payEntryFee() {
  Facility beFitGym = new Facility("Be Fit Gym", Facility.GYM);
  Visit visit = new Visit(beFitGym, 2);
  Client client = new Client("Mike");

  // when
  client.addVisit(visit);
  String payment = client.getReceipt();

  // Then
  new FacilityAssetion().assertFacility(payment, "Be Fit Gym", 4.0, 100);
}

@Test
public void twoHours_PayForEach() {
  // Given
  Facility beFitGym = new Facility("Jacuzzi", Facility.STEAM_BATH);
  Visit visit = new Visit(beFitGym, 2);
  Client client = new Client("Mike");

  // When
  client.addVisit(visit);
  String payment = client.getReceipt();

  // Then
  new FacilityAssetion().assertFacility(payment, "Jacuzzi", 10.0, 150);
}

Następnie wykonuję ponownie ekstrakcję metody „assertFacility”. Dzięki tem wywołanie kontruktora assercji znajdzie się się tyko w jednym miejscu. Poniżej ostateczny rezultat

private void assertFacility(String payment, String facilityName, 
      String facilityPrice, String facilityPoints) {
        new FacilityAssertion()
          .assertFacility(payment, facilityName, 
                          facilityPrice, facilityPoints);
    }

@Test
public void twoHours_isOnly_payEntryFee() {
  Facility beFitGym = new Facility("Be Fit Gym", Facility.GYM);
  Visit visit = new Visit(beFitGym, 2);
  Client client = new Client("Mike");

  // when
  client.addVisit(visit);
  String payment = client.getReceipt();

  // Then
  assertFacility(payment, "Be Fit Gym", 4.0, 100);
}

@Test
public void twoHours_PayForEach() {
  // Given
  Facility beFitGym = new Facility("Jacuzzi", Facility.STEAM_BATH);
  Visit visit = new Visit(beFitGym, 2);
  Client client = new Client("Mike");

  // When
  client.addVisit(visit);
  String payment = client.getReceipt();

  // Then
  assertFacility(payment, "Jacuzzi", 10.0, 150);
}

Przenieś parametr z metody do konstruktora

Konstruktor jest obecnie wywoływany tylko z jednego miejsca. Dodaję więc nowy parametr w kontruktorze, następnie pole w klasie. Kiedy metoda korzysta już z pola „payment” zamiast z parametru „payment” – to usuwam niepotrzebny parametr.

Zamień konstruktor na wywołanie metody statycznej

W daljszej kolejności w klasie FacilityAssertion uruchamiam automatyczne przekształcenie „Zamień wywołanie konstruktora metodą statyczną”.

public class FacilityAssertion {
  private String payment;

  private FacilityAssertion(String payment) {
     this.payment = payment;
  }

  public static FacilityAssertion assertThat(String payment) {
      return new FacilityAssertion(payment);
  }

  void hasAttributes(String facilityName, String facilityPrice, 
     String facilityPoints) {
    XmlAssert.assertThat(this.payment)
      .valueByXPath("/table/tr[1]/td[1]")
      .isEqualTo(facilityName);

    XmlAssert.assertThat(this.payment)
      .valueByXPath("/table/tr[1]/td[2]")
      .isEqualTo(facilityPrice);

    XmlAssert.assertThat(this.payment)
      .valueByXPath("/table/tr[1]/td[3]")
      .isEqualTo(facilityPoints);
  }
}

Zamień metodę na łańcuch metod

Czas na utowrzenie łańcucha metod. Wykonuję więc już ostatnią ekstrakcję kilku nowych metod i dodaję na ich końcu „return this”. To umożliwi mi połączenie tych metod w łańcuch wywołań.

public class FacilityAssertion {
  private String payment;

  private FacilityAssertion(String payment) {
    this.payment = payment;
  }

  public static FacilityAssertion assertThat(String payment) {
    return new FacilityAssertion(payment);
  }

  FacilityAssertion hasAttributes(String facilityName, 
    String facilityPrice, 
    String facilityPoints) {
      return hasName(facilityName)
              .hasPrice(facilityPrice)
              .hasPoints(facilityPoints);
  }

  FacilityAssertion hasPoints(String facilityPoints) {
    XmlAssert.assertThat(this.payment)
      .valueByXPath("/table/tr[1]/td[3]")
      .isEqualTo(facilityPoints);
    return this;
  }

  FacilityAssertion hasPrice(String facilityPrice) {
    XmlAssert.assertThat(this.payment)
     .valueByXPath("/table/tr[1]/td[2]")
     .isEqualTo(facilityPrice);
    return this;
  }

  FacilityAssertion hasName(String facilityName) {
    XmlAssert.assertThat(this.payment)
     .valueByXPath("/table/tr[1]/td[1]")
     .isEqualTo(facilityName);
    return this;
  }
}

Wchłonięcie pierwotnej metody asercji

@Test
public void twoHours_isOnly_payEntryFee() {
  Facility beFitGym = new Facility("Be Fit Gym", Facility.GYM);
  Visit visit = new Visit(beFitGym, 2);
  Client client = new Client("Mike");

  // when
  client.addVisit(visit);
  String payment = client.getReceipt();

  // Then
  assertThat(payment)
    .hasName("Be Fit Gym")
    .hasPrice("4.0")
    .hasPoints("100");
}

@Test
public void twoHours_PayForEach() {
  // Given
  Facility beFitGym = new Facility("Jacuzzi", Facility.STEAM_BATH);
  Visit visit = new Visit(beFitGym, 2);
  Client client = new Client("Mike");

  // When
  client.addVisit(visit);
  String payment = client.getReceipt();

  // Then
  assertThat(payment)
    .hasName("Jacuzzi")
    .hasPrice("10.0")
    .hasPoints("150");
}

Zastosuj analogicznie wzorzec budowniczego lub fabryki do konfiguracji testów

Z pewnością zauważyłeś że teraz konfiguracje testów różnią się pomiędzy sobą tylko typem pomieszczenia oraz ilością godzin. Zwracana nazwa pomieszczenia jest zawsze taka sama więc można sprawdzić ją osobno i tylko tylko raz.

@Test
public void twoHours_isOnly_payEntryFee() {
  // Given
  String payment = newPaymentFor(Facility.GYM, 2);

  // Then
  assertThat(payment)
    .hasPrice("4.0")
    .hasPoints("100");
}

@Test
public void twoHours_PayForEach() {
  // Given
  String payment = newPaymentFor(Facility.STEAM_BATH, 2);

  // Then
  assertThat(payment)
    .hasPrice("10.0")
    .hasPoints("150");
}

Jak widać uzyskaliśmy czyste testy. Nie zawierają duplikacji kodu, są łatwe do zrozumienia. Napisanie kolejnego testu jest także proste.

Biblioteki promujące wzorzec fluent builder

Jedną z bibliotek promujących płynne asercje jest assertJ. Biblioteka ta dobrze integruje się z JUnit. Umożliwia budowanie własnych modułów płynnych asercji . Ułatwia napisanie jednego szczegółowego komunikatu w przypadku niepowodzenia testu lub zwrócenie nowej zagnieżdżonej instancji asercji, która sprawdza na przykład kolejne dane w teście integracyjnym lub jednostkowym.

Dbaj o czytelność testów

Wujek Bob kiedyś powiedział (albo napisał) : „traktuj swoje testy jak obywatela pierwszej kategorii”. Dbaj więc o testy poprzez ich ciągłą refaktoryzację! Czysty Kod to także Czyste Testy!

Pamiętaj, że koncepcja piramidy refaktoryzacji jak i zasady SOLID mają takie samo zastosowanie przy refaktoryzacji testów!

Spread the word. Share this post!