Java Developer? Przejdź na wyższy poziom wiedzy 🔥💪  Sprawdź

Team Leader? Podnieś efektywność swojego zespołu 👌 Sprawdź

Clean Code w testach: klasy Steps

utworzone przez Java, Spring Framework, Testowanie

Podczas swojej pracy programista częściej czyta kod, niż go pisze. Kod testowy jest żywą specyfikacją systemu. Jeżeli jest czytelnie napisany, możesz z niego czerpać wiedzę na temat tego, w jaki sposób działa aplikacja. Powinieneś znaleźć tam kluczowe przypadki użycia. Dlatego postaraj się, aby kod testów był pisany z odpowiednią dbałością, jak kod produkcyjny. A może i nawet większą, bo statystycznie jest go więcej.

W tym artykule chcę się podzielić z Tobą przemyśleniami na temat hermetyzacji i odpowiedniemu nazewnictwu powtarzalnych fragmentów kodu.

Extract method

Jednym ze sposobów poprawy czytelności w kodzie wyciąganie części wspólnych lub bardziej złożonych, które mogą być nazwane, do osobnych funkcji i klas.

Wyobraź sobie fragment testów aplikacji koszyka sklepowego, który w wielu przypadkach zakłada, że jakiś produkt musi istnieć. Jest to warunek wstępny do wielu testów:

@Test
void insertsProductToBasket() throws Exception {
    Basket basket = Baskets.emptyBasket();
    given(productRepository.getProduct(soap.getProductId())).willReturn(Optional.of(soap));
    
    // ...
}

@Test
void increasesProductCountWhenProductAlreadyExistsInBasket() throws Exception {
    Basket basket = Baskets.withProduct(soap);
    given(productRepository.getProduct(soap.getProductId())).willReturn(Optional.of(soap));
    
    // ...
}

// ...

W powyższym kodzie powtarza się fragment, który zakłada, że produkt istnieje w repozytorium.

Zauważając tę sytuację używamy refaktoryzacji extract method wyciągając kod do osobnej, prywatnej metody givenWeHaveProduct w teście, aby zwiększyć jego czytelność.

@Test
void insertsProductToBasket() throws Exception {
    Basket basket = Baskets.emptyBasket();
    givenWeHaveProduct(soap);
    
    // ...
}

@Test
void increasesProductCountWhenProductAlreadyExistsInBasket() throws Exception {
    Basket basket = Baskets.withProduct(soap);
    givenWeHaveProduct(soap);
    
    // ...
}

// ...

private void givenWeHaveProduct(Product product) {
    given(productRepository.getProduct(product.getProductId())).willReturn(Optional.of(product));
}

Kod zaczyna wyglądać bardzo czytelnie – niczym dokumentacja systemu.

Istnienie produktu w repozytorium jest na tyle częstym założeniem, że pojawia się ono pomiędzy testami. Co wtedy? Lub gdy na poziomie testów komponentowych (o piramidzie testów przeczytasz tutaj) chcemy powielać takie założenie?

Extract class

W implementacji BDD (Behaviour-Driven-Development) przez framerowk Cucumber istnieje koncepcja wprowadzania klas Steps, które mapują język naturalny w scenariuszach testowych napisanych prozą na wykonywalny kod. Wygląda to mniej więcej tak:

public class ProductRepositorySteps {
 
    @Given("^Product (.*) with id (.*) exists.$")
    public void product_exists(String name, String id) {
        // ...
    }
    

Gdyby zaczerpnąć podobny koncept do zestawu testów, moglibyśmy mieć klasę ProductRepositorySteps o następującej treści:

@RequiredArgsConstructor
public class ProductRepositorySteps {

    private final ProductRepository productRepository;

    public void givenWeHaveProduct(Product product) {
        given(productRepository.getProduct(product.getProductId())).willReturn(Optional.of(product));
    }
}

Tym sposobem wyekstrahowałem powtarzalną funkcjonalność i mogę używać jej w wielu testach:

@SpringBootTest
@Import(TestConfig.class)
class BasketControllerTest {

    @Autowired
    private ProductRepositorySteps productRepositorySteps;
    
    @Test
    void insertsProductToBasket() throws Exception {
        Basket basket = Baskets.emptyBasket();
        productRepositorySteps.givenWeHaveProduct(product);
        
        // ...
    }

    @Test
    void increasesProductCountWhenProductAlreadyExistsInBasket() throws Exception {
        Basket basket = Baskets.emptyBasket();
        productRepositorySteps.givenWeHaveProduct(product);
        
        // ...
    }
}

Psst… Interesujący artykuł?

Jeżeli podoba Ci się ten artykuł i chcesz takich więcej – dołącz do newslettera. Nie ominą Cię materiały tego typu.

.

Konfiguracja

Aby wszystko zadziałało, tworzę konfigurację testową, w której:

  1. definiuję bean ProductRepositorySteps,
  2. definiuję mocka implementacji ProductRepository za pomocą @MockBean,
  3. załączam konfigurację aplikacji, np. ApplicationConfig.class.
@Configuration
@Import(ApplicationConfig.class)
public class TestConfig {

    @MockBean
    ProductRepository productRepository;

    @Bean
    ProductRepositorySteps productRepositorySteps() {
        return new ProductRepositorySteps(productRepository);
    }
}

Aby uniknąć pułapki 100% Code Coverage i płonącej produkcji (testowania kodu skonfigurowanego w inny sposób w testach, a w inny na produkcji), w testach komponentowych posiadam w pełni skonfigurowaną aplikację, importując produkcyjny config ApplicationConfig.class. Abym mógł testować w izolacji, zamieniam tylko implementacje I/O na mocki (np. serwisy aplikacyjne pobierające dane z innych mikrousług lub bazę danych in-memory).

W podobny sposób możesz wyodrębniać powtarzający się kod weryfikujący (część asercji). Może się okazać, że tam też odnajdziesz powielające się schematy, np.: thenBasketIsSaved.

@Test
void insertsProductToBasket() throws Exception {
    // ...
    basketSteps.thenBasketIsSaved(basket);
}

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.

Podsumowanie

Kod częściej czytamy, niż go piszemy. Kodu testowego jest zazwyczaj więcej niż produkcyjnego. Poprzez stosowanie zasad Clean Code w testach możesz osiągnąć czytelne testy brzmiące jak specyfikacja systemu. Pomogą Ci w tym ekstrakcja metod oraz klas steps – dobrze nazwane klasy/metody w stylu BDD. Pamiętaj, aby w testach komponentowych konfiguracja była taka sama, jak produkcyjna, z podmienionymi interakcjami z zewnętrznymi systemami/modułami. Unikniesz wtedy testowania kontekstu aplikacji innego niż produkcyjny.

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 👌

Obrazek: Strzałka zdjęcie utworzone przez freepik – pl.freepik.com

Dyskusja