lukaszstaniszewski.pl

blog programistyczny

lukaszstaniszewski.pl

blog programistyczny

Testy jednostkowe & TDD – wprowadzenie

W dzisiejszym wpisie powiem Ci, czym tak naprawdę są testy jednostkowe oraz TDD (Test-driven development), czyli moim zdaniem obowiązek gdy, programujesz w PHP’e, ale również w innych językach, czy to w Javie, Pythonie lub innym języku.

Napiszemy również dwa proste testy, jeden w PHP z użyciem PHPUnit a następnie w Javie z użyciem jUnit.

 

Widziałeś kiedyś mema, który mówi “W C++ o błędach mówi, nam kompilator w PHP klient”? Ten mem tak naprawdę to kwintesencja, dlaczego brak testów w twoim projekcie może doprowadzić do katastrofy. Testy jednostkowe w skrócie, służą do testowania naszego kodu i wykrywaniu, błędu a dokładniej jego funkcjonalności. TDD to podejście w którym testy są napisane przed rozpoczęciem pisania właściwego kodu tj. logiki biznesowej. Całość testów nie jest napisana w 100% a jest uzupełniania wraz z logiką biznesową naszej klasy. Robimy tak ponieważ bez sensu byłoby by myśleć czy serwis X będzie nam potrzebny bo może się nagle okazać że jednak lepiej to będzie wrzucić do traita. Testy piszemy aby uniknąć błędów logicznych, np. błędnie postawiony znak większości w if’ie. 

 

Wyobrażasz sobie manualne testowanie każdego elementu kodu po kolei za każdym wdrożeniem? Niezbyt, prawda? A jeżeli tak robisz, to Ci współczuję. Mógłbyś o czymś zapomnieć, a w razie buga szukanie błędu może troszkę zająć. Dodatkowo byłoby to czasochłonne i kosztowne, ale to też nie znaczy, że testy jednostkowe są tanie w utrzymaniu, jest to w końcu pisanie kodu oraz zrozumienie dogłębnie logiki klasy, a co za tym idzie to też kosztuje. Dodatkowo musisz pamiętać, że drobna zmiany klasy na rzecz danej funkcjonalności może za sobą pociągnąć lawinę błędów, której ty możesz nie zauważyć testując manualnie.

 

Jak zacząć pisać testy zgodnie z podejściem TDD?

By zacząć pisać testy musisz zrozumieć następujące trzy fazy tworzenia kodu:

  1. Piszesz testy (Red)
  2. Piszesz klasę (Green)
  3. Refactoring – oczyszczanie kodu (Blue)

 

Red – Tworzymy test w którym testujemy za pomocą mokowania (atrapa obiektu), wstrzykiwania zależności (Dependency injection), asercji itd. funkcjonalności naszej klasy. Gdy uruchamiamy testy, świecą one na czerwono (testy nie przechodzą!), tak powinno być nie musisz się bać że to zła oznaka.

 

Green – Piszemy klasę, co powoduję że testy przechodzą na zielono. Nie musimy się skupiać nad czystością naszego kodu, mamy w możliwie jak najszybszy sposób dostarczyć klasę tak by testy świeciły się na zielono.

 

Blue – Sprawdzamy gotowy kod klasy oraz testy. I tu się zatrzymajmy, czym nazwiemy sprawdzenie kodu (Refactoring)? Jest to sprawdzenie m.in. czy zmiennym nadane są odpowiednie modyfikatory widoczności (public, protected, private), czy kod spełnia zasady KISS, DRY, SOLID, PSR etc. Na tym etapie już nie zmieniamy funkcjonalności kodu, po prostu go czyścimy.

 

Kiedy nie warto pisać testów?

Fundamentalne pytanie, które sam sobie często zadaję, czyli kiedy nie warto pisać testów i powiedzieć sobie, pomijam testy.

 

Zacznijmy od Dependency Injection, moim zdaniem testowanie setterów, getterów nie ma najmniejszego sensu. Jeżeli posiadamy prosty event listener, który wywołuję tylko getter (przykład: kliknij tutaj) to robienie do tego testów, jest tylko marnowaniem czasu. Podany tutaj Event Listener jest tylko przykładem odnosi się do wszystkich klas używających wzorca projektowego DI (Dependency Injection).

 

Jak dobrze wiemy encje (eng. Entity) to nic innego jak DI z adnotacjami (ang. Annotations), moim zdaniem tak samo nie musimy testować encji.

 

Ale jak to bywa w IT, to wszystko zależy, to ty musisz zdecydować co testować a co nie. Nie ma jednej receptury, byłoby to zbyt proste.

Co to są asercje? 

Asercje w wielkim skrócie, służą do sprawdzania zwracanej wartości. Użycie np. assertEquals, sprawdza czy zwracana wartość, jest identyczna do tej oczekiwanej. Jako drugi przykład podamy assertNull, sprawdza on, czy zwracana wartość (podana w argumencie), jest równa null. Więcej asercji oraz przykłady ich użycia znajdziesz tutaj.

 

Co będziemy testować i jak do tego podejdziemy?

Twoją przygodę z testami jednostkowymi, zaczniemy od prostego przykładu, nasz skrypt będzie obliczał silnie (spokojnie, do bardziej zaawansowanych jeszcze kiedyś przejdziemy, nie od razu Rzym zbudowano 😉 ).

Zacznijmy od pierwszego kroku, a dokładniej zidentyfikowania co skrypt może wykonać nie tak. Pierwszą i jedyną opcją która może pójść nie tak jest pojawienie się wyjątku gdy podamy ujemną silnie.
Mamy więc punkt pierwszy, wiemy już że musimy zbadać dwa przypadki, gdy podamy odpowiednią silnie (metoda działa prawidłowo), oraz ujemną (pojawia się wyjątek).
Zapisujemy sobie to w dowolnym miejscu, kartka, komentarz, nazwa metody, obojętnie gdzie. Najważniejsze jest to żebyś ty wiedział co masz testować.

 

Czy nasuwa Ci się pytanie, co dalej? Zakasamy rękawy, czeka nas najprzyjemniejsza część TDD czyli idziemy kodować! 🙂

 

Napiszmy test w PHP

Zaczniemy od napisania composer.json

{
  "require": {
    "php": "7.3.*"
  },
  "require-dev": {
    "phpunit/phpunit": "7.5.*"
  },
  "autoload": {
    "psr-4": {
      "App\\": "./src"
    }
  },
  "autoload-dev": {
    "psr-4": {
      "App\\Test\\": "./tests"
    }
  }
}

Mamy już gotowy composer.json, więc uruchamiamy composer’a (Komenda w konsoli: composer update) i pobieramy PHPUnit!

Przyszedł czas na plik konfigurujący PHPUnit, bez którego sobie nie poradzi. W głównym katalogu twojego projektu musisz utworzyć plik phpunit.xml

<?xml version="1.0" encoding="UTF-8"?>
<!-- https://phpunit.de/manual/current/en/appendixes.configuration.html -->
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/4.7/phpunit.xsd"
         backupGlobals="false"
         colors="true"
         bootstrap="vendor/autoload.php"
>
    <php>
        <ini name="error_reporting" value="-1" />
    </php>

    <testsuites>
        <testsuite name="Project Test Suite">
            <directory>tests</directory>
        </testsuite>
    </testsuites>

    <filter>
        <whitelist>
            <directory>src</directory>
        </whitelist>
    </filter>
</phpunit>

Mamy już wszystko co potrzebne by rozpocząć twoją przygodę z TDD. Przyszedł więc czas na napisanie twojego pierwszego a może już któregoś testu!

<?php
declare(strict_types=1);

namespace App\Test;

use App\StrongCalc;
use PHPUnit\Framework\TestCase;
use \Exception;

/**
 * Class StrongCalcTest
 */
class StrongCalcTest extends TestCase
{

    /**
     * @var StrongCalc
     */
    private $strongCalc;

    /**
     *
     */
    public function setUp(): void
    {
        $this->strongCalc = new StrongCalc();

        parent::setUp();
    }

    /**
     * @throws Exception
     */
    public function testCalc(): void
    {
        $this->assertEquals(6, $this->strongCalc->calc(3));
    }

    /**
     * @throws Exception
     */
    public function testNegativeStrong(): void
    {
        $this->expectException(Exception::class);
        $this->strongCalc->calc(-3);
    }
}

Sprawdźmy teraz za pomocą polecenia php vendor/bin/phpunit czy test się załamuję. Testy świecą na czerwono? To dobrze, zaraz będą na zielono. 🙂

Został więc ostatni element układanki, implementacja klasy.

<?php
declare(strict_types=1);

namespace App;

use \Exception;

/**
 * Class StrongCalc
 * @package App
 */
class StrongCalc
{
    /**
     * @param int $strong
     * @return int
     * @throws Exception
     */
    public function calc(int $strong): int
    {
        if ($strong <= 0) {
            throw new Exception("Strong can't be negative!");
        }

        $strongResult = 1;

        for ($i = 1; $i <= $strong; ++$i) {
            $strongResult *= $i;
        }

        return $strongResult;
    }
}

Zobaczmy teraz rezultat całego naszego kodowania, za pomocą identycznego polecenia co przy czerwonym teście (faza RED), sprawdź czy testy świecą na zielono. Świecą na zielono? Brawo ty! 🙂

jUnit, czyli piszemy testy jednostkowe w Javie

Tak samo jak w przypadku PHP’e zaczniemy od zbudowania pliku package manager’a (w głównym katalogu projektu utwórz plik o nazwie: pom.xml) a dokładniej użyjemy do tego Maven’a.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com</groupId>
    <artifactId>lukaszstaniszewski</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
    
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.0</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

Okej, mamy gotowy plik pom.xml, więc pobierzmy jUnit (Komenda w konsoli: mvn dependency:resolve)
Drugim krokiem będzie napisanie testu naszej klasy obliczającej silnie. Do dzieła!

package lukaszstaniszewski;

import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.assertEquals;

/**
 * The type Strong calc test.
 */
public class StrongCalcTest {

    private StrongCalc strongCalc;

    /**
     * Sets up.
     */
    @Before
    public void setUp() {
        this.strongCalc = new StrongCalc();
    }

    /**
     * Calc.
     *
     * @throws Exception the exception
     */
    @Test
    public void calc() throws Exception {
        assertEquals(6, this.strongCalc.calc(3));
    }

    /**
     * Negative strong.
     *
     * @throws Exception the exception
     */
    @Test(expected = Exception.class)
    public void negativeStrong() throws Exception {
        this.strongCalc.calc(-3);
    }
}

Sprawdzamy oczywiście czy test się załamuję komenda mvn test
Test się załamuję? Super więc piszemy klasę!

package lukaszstaniszewski;

/**
 * The type Strong calc.
 */
public class StrongCalc {

    /**
     * Calc int.
     *
     * @param strong the strong
     * @return the int
     * @throws Exception the exception
     */
    public int calc(int strong) throws Exception{
        if (strong <= 0) {
            throw new Exception();
        }

        int strongResult = 1;

        for (int i = 1; i <= strong; ++i) {
            strongResult *= i;
        }

        return strongResult;
    }

}

Tak samo jak w przykładzie z PHP sprawdzamy czy testy świecą na zielono (Komenda: mvn test). Jeżeli świecą na zielono to nic mi innego nie zostało jak tylko gratulować tobie!

Podsumowanie

W dzisiejszym wpisie wytłumaczyłem Ci, na czym polega Test-driven development oraz testy jednostkowe. Stworzyliśmy wspólnie po jednym teście w PHP i Javie co jest dobrą podstawą żebyś zaczął testować swój projekt. Jeżeli zaciekawił Cię temat TDD i/lub testów jednostkowych, musisz dowiedzieć się również co to jest mokowanie [KLIKNIJ TUTAJ]

Źródła:

  •  TDD. Sztuka tworzenia dobrego kodu – Kent Beck

Do zobaczenie już za niedługo!
Pozdrawiam Łukasz „Ferdyrurka” Staniszewski!

Testy jednostkowe & TDD – wprowadzenie
Przewiń na górę