Nie mogę napisać testu do metody – part 2.

24-08-2022

W poprzednim artykule Nie mogę napisać testu do metody – part 1 zajmowaliśmy się problemem jakim jest napisanie testu do metody, która w środku tworzy nowe obiekty.

Dziś rozważymy sytuację w której ta metoda nie ma tworzenia nowych obiektów, ale jest prywatna (private method).

Test do metody prywatnej? JAK TO?!

Wyobraź sobie sytuację w której masz logikę wyliczania nowej ceny dla konkretnego produktu w metodzie prywatnej. Chcesz przetestować ten newralgiczny fragment kodu, aby ceny dobrze się wyliczały i co teraz? Poniżej znajdziesz schemat jak można podejść do takiej sytuacji w zależności od tego jak wygląda Twój projekt. Schemat ten został przygotowany na podstawie książki „Praca z zastanym kodem. Najlepsze techniki” autora Michael C. Feathers. Autor w książce opisuje dokładnie ten proces, a poniższy schemat jest skróconą interpretacją rozdziału.

Algorytm napisania testu do metody prywatnej

 

Algorytm napisania testu do metody prywatnej

Schemat

Czy możesz napisać test do metody publicznej, która wykorzystuje tą metodę prywatną?

Napisanie testu do metody publicznej wykorzystującej prywatną, która nas najbardziej interesuje jest najlepszą sytuacją. Nie zmieniamy tutaj metody prywatnej, a jednocześnie pokrywamy testami metodę publiczną. Niestety czasami do metody publicznej jest bardzo ciężko napisać test, czy to ze względu na jej skomplikowanie kodu, czy tworzenie nowych obiektów. Wtedy musimy przejść do kolejnego pytania.

Czy metoda ta może zamienić się w publiczną?

W niektórych sytuacjach taka zmiana ma sens, ale to dużo zależy od konkretnej sytuacji. Czy ta metoda powinna być publiczna? Czy inne klasy będą się do niej odwoływać? Czy odwołanie się do niej spowoduję zmianę obiektu i wpłynie negatywnie na obiekt? Jeśli dochodzimy do wniosku, że zmiana ta może przynieść negatywny wpływ przechodzimy do kolejnego pytania.

Czy klasa ta nie robi za dużo i może metoda powinna zostać przeniesiona do nowej klasy i tam będzie metodą publiczną?

W sytuacji, gdy metoda nie jest mocno związana z innymi metodami klasy, bądź zmiennymi i może stać się osobnym bytem, można pomyśleć nad wyniesieniem jej do osobnej klasy. Ale co jeśli jest zbyt powiązana? Wtedy przechodzimy do kroku „Zmień dostęp metody z prywatnej na chronioną i napisz test„, który szerzej opisuję w sekcji poniżej.

Przykład

Wydaje mi się, że większość sposobów rozwiązania („Napisz test do metody publicznej” / „Napisz test do nowej metody publicznej” / „Utwórz nową klasę i napisz test do metody”)  nie potrzebuje przykładu (ale może powinny być dodane? Napisz jeśli przykłady by Ci się przydały) to o tyle „Zmień dostęp metody prywatnej na chronioną i napisz test” może wywoływać u Ciebie mieszane uczucia. W związku z powyższym postaram się przeprowadzić Cię po tym procesie. Przykład jest w języku PHP, ale podejrzewam, że w Twoim języku można zastosować bardzo podobny manewr.

Stan początkowy

Na początku mamy metodę prywatną calculateNewPrice.

class Legacy
{
    public function __construct(
        private readonly ?int $customerDiscount = null
    ) {
    }

    // dużo różnych metod prywatnych i publicznych, dużo legacy....

    private function calculateNewPrice(Money $currentPrice, \DateTime $date): Money
    {
        if ($date->format('N') === '5') {
            $currentPrice = $currentPrice->add(new Money(200, $currentPrice->getCurrency()));
        }

        if ($this->customerDiscount) {
            $discount = $currentPrice->multiply($this->customerDiscount);
            $discount = $discount->divide(100);
            $newPrice = $currentPrice->subtract($discount);
        }

        return $newPrice ?? $currentPrice;
    }
}

Krok 1

Zmieniamy jej modyfikator dostępu z private na protected.

class Legacy
{
    public function __construct(
        private readonly ?int $customerDiscount = null
    ) {
    }

    protected function calculateNewPrice(Money $currentPrice, \DateTime $date): Money
    {
        if ($date->format('N') === '5') {
            $currentPrice = $currentPrice->add(new Money(200, $currentPrice->getCurrency()));
        }

        if ($this->customerDiscount) {
            $discount = $currentPrice->multiply($this->customerDiscount);
            $discount = $discount->divide(100);
            $newPrice = $currentPrice->subtract($discount);
        }

        return $newPrice ?? $currentPrice;
    }
}

Krok 2

Tworzę klasę testu. W teście tworzę klasę anonimową, która rozszerza naszą klasę bazową. Nadpisuję metodę calculateNewPrice, aby w teście była publiczna. Teraz mogę napisać test, który widzisz poniżej 🙂

class LegacyTest extends TestCase
{
    public function testCalculatePriceWhenTenPercentageDiscount(): void
    {
        $after = new class (10) extends Legacy
        {
            public function calculateNewPrice(Money $currentPrice, \DateTime $date): Money
            {
                return parent::calculateNewPrice($currentPrice, $date);
            }
        };

        $originalPrice = Money::PLN(1000);
        $newPrice = Money::PLN(900);
        $monday = new \DateTime('2022-07-18');
        $this->assertEquals($newPrice, $after->calculateNewPrice($originalPrice, $monday));
    }

    public function testCalculatePriceWhenNoDiscount(): void
    {
        //...
    }

    public function testCalculatePriceOnFridayWhenNoDiscount(): void
    {
        // ...
    }

    public function testCalculatePriceOnFridayWithDiscount(): void
    {
        // ...
    }
}

GitHub

Cały przykład znajdziesz na moim GitHub.

Uwagi końcowe

Reflection Class

W kroku z przykładu powyżej został użyty manewr zmiany modyfikacji dostępu (z private na protected). Niektórzy mogą zauważyć że podobny efekt osiągnie się wykorzystując refleksję (ReflectionClass). W PHP użycie refleksji w mojej opinii zaciemnia obraz aplikacji. Refaktoryzację (proces modyfikacji projektu, mającej na celu zwiększenie jakości aplikacji, bez zmiany funkcjonalności) zazwyczaj przeprowadzamy w cyklach, nie zmieniamy wszystkiego jednocześnie (chociaż z tego co słyszałam są też i fani zmiany wszystkiego na raz – co często prowadzi do długożyjących gałęzi które potem trudno jest utrzymywać – dlatego ja nie jestem fanką takiego podejścia, ale kto co lubi 😉 ). W takiej sytuacji jeśli dziś napiszesz test nie modyfikując modyfikatora dostępu z prywatnej na chronioną, tylko użyjesz refleksji, to później może się okazać, że test Ci nie przechodzi bo na pierwszy rzut oka nie wiesz, że metoda jest gdzieś wykorzystywana bo Twoje IDE Ci może nie podpowiedzieć, że dana metoda jest gdzieś wykorzystywana.

Jednakże jest to moja opinia 😉 Masz inne zdanie na ten temat? Podziel się w komentarzu! 😉

PS Michael C. Feathers wyraził swoją opinię w książce na temat użycia refleksji. Wypowiedź ta jest dość długa, więc pozwolę sobie tutaj jej nie przytaczać, a wolałabym nie wyciągać z niej fragmentu.

Książka

Jeśli zainteresował Cię powyższy artykuł, to zachęcam Cię do zapoznania się z artykułem ’Pracujesz z kodem legacy? Książka „Praca z zastanym kodem. Najlepsze techniki”’ w którym opisuję co możesz znaleźć w  książce „Praca z zastanym kodem. Najlepsze techniki”. Chcesz, abym przybliżyła jakiś inny rozdział z książki? Daj znać w komentarzu 😉


Źródła

  1. Książka: Praca z zastanym kodem. Najlepsze techniki

Do przedstawienia algorytmu zostało wykorzystane narzędzie: miro.

 

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Witryna wykorzystuje Akismet, aby ograniczyć spam. Dowiedz się więcej jak przetwarzane są dane komentarzy.