Już dostępny

Program Szkoleniowy Java Developer dostępny 🔥💪 tylko TERAZ za 999 zł  Sprawdź szczegóły i agendę

Zakres

Monitoring • Apache Kafka • Clean Code Testowanie • Hibernate • Systemy kolejkowe Sprawdź szczegóły i agendę

Zakres

14 modułów  /  ponad 40h nagrań  /  230 lekcji  /  dożywotni dostęp  /  Sprawdź szczegóły i agendę

Dane testowe: Matki Obiektów i Test Buildery vs Mocki, Stuby

utworzone przez Java, Testowanie

[Szybkie info]: Startujemy z II edycją Programu Szkoleniowego Java Developera 🚀. To MEGA piguła wiedzy o Java 🔥💪

  • 14 tygodniowy program szkoleniowy online,
  • 230 lekcji w formie video (40 godzin materiału)
  • z dożywotnim dostępem
  • Case Studies, masz dostęp do kodu i obrazów Dockerowych
  • zamknięta grupa mentorzy + uczestnicy i webinary na żywo

W agendzie znajdziesz: Mikroserwisy, Systemy kolejnowe, Apache Kafka, Caching, Hibernate/MyBatis/Spring Data, techniki efektywnych Testów kodu, Clean Code i Maven.

Tylko teraz dołączysz z 63% rabatem to 2699 zł 999 zł (+VAT). I nigdy już nie będzie taniej. Poniżej dowiesz się więcej:

Zobacz więcej

A teraz przechodzimy do artykułu:

Pisząc testy, staramy się szukać części wspólnych i zamykać powtarzalne fragmenty. Jednym z tych miejsc jest tworzenie danych testowych. Łatwo można sobie wyobrazić sytuację, w której w wielu testach powtarzany jest fragment kodu:

Basket basket = new Basket();

Product product = Product.builder()
  .productId(UUID.randomUUID())
  .name("The product")
  .price(BigDecimal.TEN)
  .build();
basket.insert(product);

Powoduje to pewne komplikacje:

  • Kod jest powielany. Zmiany w konstrukcji obiektów dotykają wiele testów.
  • Model wcale nie musi być spójny pomiędzy testami. Będąc spójnym mam na myśli poprawne ustawienie pewnych parametrów z punktu widzenia stanu obiektów w naszej domenie. Nadpisanie pewnych cech może pozbawić dane sensu w ogóle.
  • Testy przestają być czytelne. Nie zawierają informacji naprawdę istotnych z punktu widzenia przypadku testowego. Zamiast tego tworzy się szum informacyjny tylko po to, aby utworzyć dane testowe.

Piguła wiedzy o najlepszych praktykach testowania w Java

Pobierz za darmo książkę 100 stron o technikach testowania w Java

Object Mother

Ten problem próbuje rozwiązać Object Mother pattern. Jest to nic innego jak zestaw fabryk tworzących obiekty. Mogą to być statyczne metody klasy, np.

  1. Baskets.emptyBasket() – pusty koszyk
  2. Baskets.withSingleProduct() – koszyk z jednym produktem
  3. Baskets.abadonedBasket() – porzucony koszyk

Podejście ma szereg zalet:

  • Kod samego testu jest o wiele bardziej czytelny, tworzenie obiektu jest przeniesione w inne miejsce.
  • Przykłady danych testowych są nazwane, od razu wiadomo, z czym mamy do czynienia.
  • Ewentualne zmiany w tworzeniu obiektów dotyczą mniejszej liczby miejsc.

Z biegiem czasu natomiast klasa Object Mother nie skaluje się dobrze w kontraście do zmienności danych w przypadkach testowych. Powstają coraz to nowsze przypadki w różnych kombinacjach, łącząc cechy nawzajem:

  1. Baskets.withProducts(...) – z produktami
  2. Baskets.withProductsAndDiscount() – z produktami i zniżką
  3. Baskets.withoutDeliveryAddressButWithDiscount() – bez (jeszcze) podanego adresu dostawy i ze zniżką

Dostrzegamy kolejne części wspólne, parametryzujemy, refaktoryzujemy. Gdy brakuje czasu, istnieje pokusa, aby wykorzystywać, już gotowe obiekty i dodatkowo mockować ich zachowania:

Basket basket = Baskets.withProductsAndDiscount();
given(basket.abadoned()).willReturn(true);

Co do zasady mockujemy rzeczy, które z punktu widzenia testu chcemy wyizolować lub nie mamy nad nimi kontroli. Pytanie natomiast, czy nasz własny model domenowy jest nie do okiełznania, że musimy wyizolować zachowanie metod?

Posiadając w projekcie bogatą domenę, zaczynamy enkapsułować pewne koncepcje w agregaty, value objecty, doprowadzamy obiekty do pewnych stanów, używając metod. Można dojść do wniosku, że tworzenie danych testowych jest całkiem uciążliwe.

Test Builders

Ten problem adresuje Test Builder pattern. Koncepcja jest prosta – tworzymy klasę buildera, która daje możliwość nadpisania konstrukcji obiektu. Implementacja może wyglądać tak:

public class BasketsBuilder {

    private List<Product> products = singletonList(defaultProduct().build());
    private String param1;

    public static BasketsBuilder builder() {
        return new BasketsBuilder();
    }

    public BasketsBuilder withProducts(List<Product> products) {
        products = products;
        return this;
    }

    public BasketsBuilder withSingleProducts(Product product) {
        products = singletonList(product);
        return this;
    }

    public BasketsBuilder withoutProducts() {
        products = emptyList();
        return this;
    }

    public BasketsBuilder withParam1(String value) {
        param1 = value;
        return this;
    }

    public Basket build() {
        Basket basket = new Basket(param1);
        products.stream().forEach(basket::insert);
        return basket;
    }
}

Koncepcja polega na tym, aby umożliwić nadpisanie dowolnego parametru, a następnie doprowadzić obiekt domenowy do oczekiwanego stanu w metodzie build().

Nic nie stoi na przeszkodzie, aby mieć już nazwane przypadki testowe i zamiast tworzyć dany obiekt, umożliwić dalsze nadpisywanie wartości dla kolejnych testów, np.

Baskets.withSingleItem() // zwraca BasketsBuilder
  .withParam1(value)
  .build();

Podoba Ci się ten artykuł? Weź więcej.

Jeżeli uważasz ten materiał za wartościowy i chcesz więcej treści tego typu – nie przegap ich i otrzymuj je prosto na swoją skrzynkę. Nawiążmy kontakt.

.

Automatyzajca pisania test builderów

Trzeba jednak powiedzieć, że pisanie test builderów to spory kawałek kodu, w dodatku powtarzalny. Każdy parametr do nadpisania to:

  • Pole w klasie buildera.
  • Metoda nadpisująca.

Problem rozwiązuje biblioteka Make-it-easy. Spójrzmy na przykład budowniczego produktu:

import com.natpryce.makeiteasy.Instantiator;
import com.natpryce.makeiteasy.Property;

public class ProductsTestBuilder {

    public static final Property<Product, UUID> ProductId = newProperty();
    public static final Property<Product, String> Name = newProperty();
    public static final Property<Product, BigDecimal> Price = newProperty();

    public static final Instantiator<Product> BasicProduct = (lookup) -> {
        return Product.builder()
                .productId(lookup.valueOf(ProductId, UUID.randomUUID()))
                .name(lookup.valueOf(Name, "Soap"))
                .price(lookup.valueOf(Price, BigDecimal.TEN))
                .build();
    };
}

Przykładowe użycie w teście wygląda całkiem dobrze, naturalnie się czyta „make a basic product”.

// static import ProductsTestBuilder.BasicProduct

Product product = make(a(BasicProduct));

Gdy chcemy nadpisać wartości, również wyrażamy się prozą: „make a basic product but with price 24”.

// static import ProductsTestBuilder.BasicProduct
// i ProductsTestBuilder.Price

Product product = make(a(BasicProduct).but(
                with(Price, new BigDecimal(24))));

Brzmi nieźle?

Cały koncept opiera się na tym, że:

  1. Tworzymy statyczne pola z pojemnikami na nawpisywanie danych, przez co nie trzeba pisać metod w test builderach: public static final Property Price = newProperty();
  2. Tworzenie obiektu zamknięte jest w lambdzie: BasicProduct = (lookup) -> {} w której budujemy obiekt i uzupełniamy go o wartości nadpisane, podając wartość domyślną, np. .price(lookup.valueOf(Price, BigDecimal.TEN))
  3. Budowanie obiektu w lambdzie odbywa się na dowolny sposób – czy jest to tworzenie przez konstruktor, czy przez wywołanie kilku metod. Na koniec otrzymujemy nowy obiekt.

Inne zalety test builderów

Konwersje danych

Dość często w naszych projektach konwertujemy struktury danych, np. z warstwy domenowej na obiekty pośrednie DTO. Dajmy na to prezentujemy adres dostawy z obiektu domenowego do DTO:

@Data
@Builder
public class AddressView {

    private String country;
    private String city;
    private String street;
    private String number;

    public static AddressView fromAddress(Address address) {
        return AddressView.builder()
                .country(address.getCountry())
                .city(address.getCity())
                .street(address.getStreet())
                .number(address.getNumber())
                .build();
    }
}

Domyślne wartości można wyeksponować do stałych Test Buildera i używać go w innych.

Tworzymy dwa test buildery (jeden dla obiektu domenowego Address, kolejny dla DTO AddressView), a domyślne wartości test buildera dla AddressView otrzymujemy takie same jak dla domenowego Address.

Co nam to daje? Już nigdy więcej kodu typu drabina assercji pole po polu:

    @Test
    void convertsAllFields() {
        Address address = make(a(AddressTestBuilder.AnyAddress));
        AddressView view = AddressView.fromAddress(address);

        assertThat(view.getCountry()).isEqualTo(address.getCountry());
        assertThat(view.getCity()).isEqualTo(address.getCity());
        assertThat(view.getStreet()).isEqualTo(address.getStreet());
        assertThat(view.getNumber()).isEqualTo(address.getNumber());
    }

Od teraz można porównywać obiekty zbudowane na podstawie tych samych danych (jeden test builder korzysta z danych innego):

    @Test
    void convertsAllFields() {
        Address address = make(a(AddressTestBuilder.AnyAddress));
        AddressView view = AddressView.fromAddress(address);
        AddressView expected = make(a(AddressViewTestBuilder.AnyAddress));

        assertThat(view)
                .usingRecursiveComparison()
                .isEqualTo(expected);
    }

Powyższy kod porównuje AddressView stworzony przez konwersję z przykładowym obiektem z test buildera pole po polu. Rozwiązanie ma zasadniczą zaletę, że gdy dodane zostałoby pole do obiektu transportowego AddressView i:

  • albo przy konwersji pole zostałoby przepisywane bez zmiany test buildera,
  • albo zostałby uaktualniony test builder, ale nie konwerter

Wtedy test zasygnalizuje błąd.

Stosując drabinę assertów nie sprawdzimy, czy w konwertowanym obiekcie znajdują się nadmiarowe nieoczekiwane wartości.

Wpis który czytasz to zaledwie fragment wiedzy zawartej w Programie szkoleniowym Java Developera od SoftwareSkill. Mamy do przekazania sporo usystematyzowanej wiedzy z zakresu kluczowych kompetencji i umiejętności Java Developera. Program składa się z kilku modułów w cotygodniowych dawkach wiedzy w formie video.

Zakończenie

Podsumowując:

  • Nazwane przypadki testowe zwiększają czytelność kodu i ograniczają liczbę miejsc do ew poprawy w przypadku zmiany sposobu tworzenia obiektów.
  • Tworzenie danych testowych może prowadzić do duplikacji, a Matki Obiektów nie są odporne na zmienność danych w przypadkach testowych.
  • Rozwiązaniem są Test Buildery umożliwiające ingerowanie w proces budowania przykładowego obiektu i ustawienie interesujących nas cech przypadku testowego.
  • Tworzenie Test Builderów można automatyzować, np. za pomocą Make-it-easy.

Piguła wiedzy o najlepszych praktykach testowania w Java

Pobierz za darmo książkę 100 stron o technikach testowania w Java

Podoba Ci się ten artykuł? Weź więcej.

Jeżeli uważasz ten materiał za wartościowy i chcesz więcej treści tego typu – nie przegap ich i otrzymuj je prosto na swoją skrzynkę. Nawiążmy kontakt.

.

Gdybyś potrzebował jeszcze więcej:

Jesteś Java Developerem?

Przejdź na wyższy poziom wiedzy 
„Droga do Seniora” 🔥💪

Jesteś Team Leaderem? Masz zespół?

Podnieś efektywność i wiedzę swojego zespołu 👌

Szybkie podsumowanie

Jak poprawić czytelność testów?

Przenoś tworzenie danych testowych do osobnych klas i metod. Pomogą Ci w tym Matki Obiektów i Test Buildery.

Czym są Matki Obiektów?

Jest to zestaw fabryk tworzących obiekty. Mogą to być statyczne metody klasy, np:

Baskets.emptyBasket() – pusty koszyk
Baskets.withSingleProduct() – koszyk z jednym produktem
Baskets.abadonedBasket() – porzucony koszyk

Podejście ma szereg zalet:
1. Kod samego testu jest o wiele bardziej czytelny, tworzenie obiektu jest przeniesione w inne miejsce.
2. Przykłady danych testowych są nazwane, od razu wiadomo, z czym mamy do czynienia.
3. Ewentualne zmiany w tworzeniu obiektów dotyczą mniejszej liczby miejsc.

Czym się różnią Test Buildery i Matki Obiektów?

Test Builder to klasa buildera, która daje możliwość nadpisania konstrukcji obiektu. Koncepcja polega na tym, aby umożliwić nadpisanie dowolnego parametru, a następnie doprowadzić obiekt domenowy do oczekiwanego stanu w metodzie build().

Literatura

Obrazek: Tło plik wektorowy utworzone przez freepik – pl.freepik.com

Dyskusja