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.