Dzisiaj chciałbym podzielić się z Tobą dwoma prostymi sposobami na poprawę testowalności kodu. Dokładniej mówiąc, sztuczki zmniejszające frustrację spowodowane ich pisaniem. Można je aplikować na różne sposoby oraz w różnych sytuacjach. W celu ich zobrazowania przygotowałem bardzo prosty przypadek. Kod nie jest najwyższej jakości, ale nie to jest istotne z puntku widzenia tego artykułu. Nie warto się na nim wzorować jeśli chodzi o aspekty biznesowe. Istotny jest proces poprawy jakości kodu. Przejdźmy zatem do działania.

Studium przypadku

Załóżmy, że w naszym projekcie istnieja klasa z dużą ilością pól. Stworzenie jej instancji, na potrzeby testów, może być nie lada wyzwaniem.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class BigObject {

  private final String name;

  private final int age;

  private final String street;

  private final String city;

  private final String country;

  // ... more fields

  public BigObject(String name, int age, String street, String city, String country) {
    this.name = name;
    this.age = age;
    this.street = street;
    this.city = city;
    this.country = country;
  }

  // ... getters
}

Dodatkowo gdzieś w kodzie powstał utils do weryfikacji czy ktoś jest osobą pełnoletnią. Tak jak napisałem na początku, nie jest to najlepszy kod. Jednak na potrzeby artykułu jest w sam punkt. IsAdultVerifier ma metodę isAdult przyjmującą obiekt klasy BigObject.

1
2
3
4
5
6
7
8
9
10
11
public final class IsAdultVerifier {

  public static final int ADULT_AGE = 18;

  private IsAdultVerifier() {
  }

  public static boolean isAdult(BigObject bigObject) {
    return bigObject.getAge() >= ADULT_AGE;
  }
}

Wszystko wygląda pięknie, ładnie. Natomiast spójrzmy co się dzieje w kodzie testowym.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class IsAdultVerifierTest {

  @Test
  void person_of_age_24_is_adult() {
    BigObject bigObject = new BigObject(
        "Jan Kowalski",
        24,
        "Krótka",
        "Kędzierzyn",
        "Polska");

    boolean isAdult = IsAdultVerifier.isAdult(bigObject);

    assertTrue(isAdult);
  }

}

Już tak kolorowo to nie wygląda. Chcemy zweryfikować czy dana osoba jest pełnoletnia. Musimy więc ją stworzyć podając wszystkie informacje. Jednak tak naprawdę z punktu widzenia testu niezbędna jest nam tylko jedna dana - wiek. Cała reszta jest szumem, który powoduje, że nie widzimy na pierwszy rzut oka co jest niezbędne do przeprowadzenia testu. Dodatkowo każda zmiana w BigObject będzie wpływała na ten test. Dojdzie płeć albo numer mieszkania - test nam się nie skompiluje. Test będzie miał więcej niż jeden powód do zmiany. Spróbujmy temu jakoś zaradzić.

Refaktoryzacja z pomocą IntelliJ

Jeśli korzystasz z IntelliJ to na starcie masz ogromną przewagę. Zaprzyjaźnij się proszę ze skrótem Ctrl + Alt + P. Dzięki niemu możemy wyekstrahować daną zmienną jako parametr metody. Zaznaczmy więc w metodzie IsAdultVerifier.isAdult następujący kod bigObject.getAge(). Wciskamy Ctrl + Alt + P, a IntelliJ zaproponuje nam nazwę dla wyciąganego parametru. Co ciekawe, od razu przedstawi wykreślenie nieużywanego parametru BigObject bigObject. Zostanie on po prostu zastąpiony przez int age.

1
2
3
public static boolean isAdult(int age) {
  return age >= ADULT_AGE;
}

Wygląda lepiej, prawda? Sprawdźmy jaki będzie to miało impakt na test.

1
2
3
4
5
6
@Test
void age_of_24_means_that_someone_is_adult() {
  boolean isAdult = IsAdultVerifier.isAdult(24);

  assertTrue(isAdult);
}

O wiele lepiej! Od razu widzimy, że pierwszoplanowym aktorem jest liczba 24. Prosta sztuczka, a jak ułatwia życie.

Pomysł z interfejsem

Chciałbym Ci zaproponować jeszcze jeden sposób. Jest on bardziej wyszukany i raczej znajdzie zastosowanie w mniejszej liczbie przypadków. Wróćmy do sytuacji z wykorzystaniem parametru BigObject bigObject. Załóżmy, że chcielibyśmy dodatkowo weryfikować czy jakieś inne stworzenie albo przedmiot jest pełnoletnie (np. ktoś chce wyprawić przyjęcie z racji 18 letniego Golfa). W takim przypadku możemy pójść w ekstrakcję parametru (pierwszy przykład) albo zastosować interfejs dostarczający wiek.

1
2
3
4
5
6
@FunctionalInterface
public interface AgeProvider {

  int getAge();

}

Posiada on tylko jedną metodę getAge. Oczywiście można tutaj skorzystać z dowolnej ilości metod. Jednak wszystko powinno być podyktowane problemem, który aktualnie rozwiązujemy. Sprawmy teraz, aby klasa BigObject implementowała ten interfejs. Wtedy jej instancje będą mogłby być przekazane do nowej metody w IsAdultVerifier, którą zaraz stworzymy.

1
2
3
4
5
public class BigObject implements AgeProvider {

  // the same code as above

}
1
2
3
public static boolean isAdult(AgeProvider ageProvider) {
  return ageProvider.getAge() >= ADULT_AGE;
}

Ciało metody nie jest niczym odkrywcznym. Jednak uzyskaliśmy możliwość przekazywania do niej wszystkich instancji klas, które implementują nasz nowopowstały interfejs. Przejdźmy zatem do kodu testowego i sprawdźmy co się tam zmieniło.

1
2
3
4
5
6
@Test
void age_provider_with_24_means_that_someone_is_adult() {
  boolean isAdult = IsAdultVerifier.isAdult(() -> 24);

  assertTrue(isAdult);
}

Wygląda on jasno i przejrzyście. Dzięki temu, że AgeProvider jest interfejsem funkcyjnym, możemy w teście skorzystać z lambdy. Oczywiście jeśli ktoś chciałby to może również stworzyć instancję każdej innej dowolnej klasy implementującej AgeProvider. Jednak teraz w tym miejscu może to się po prostu mijać z celem…

Podsumowanie

Jak widać proste sztuczki mogą nam naprawdę uprościć życie. Dzięki niektórym z nich możemy w mniejszym stopniu wylewać naszą frustrację na testy. Jednak powstaje pytanie - “czy one naprawdę są czemuś winne?”. Testy powstały po to, aby nam pomagać. Jeśli mamy z nimi jakiś kłopot, to jest to po prostu ich krzyk o to, że coś może jest nie tak z designem naszej aplikacji. Jednak o tym więcej powiedziałem na swojej prelekcji z PHPCon’22.