Pomoc - Szukaj - Użytkownicy - Kalendarz
Pełna wersja: [PHP] Testy jednostkowe w PHPUnit
Forum PHP.pl > Forum > PHP > Object-oriented programming
eerie
Na co powinienem zwrócić uwagę, pisząc testy w PHPUnit? Co powinienem testować, a czego nie?

Napisałem kiedyś taki test kontrolera w Symfony. Jak przetestować klikanie w linki bez Symfony?

Przeglądam pisanie testów w PHPUnit i nie widzę przykładów, jak to zrobić.

Podpowiedzcie mi, co powinienem przetestować w mojej aplikacji... smile.gif
nospor
Cytat
Jak przetestować klikanie w linki bez Symfony?

Ja do tego uzywam Panther
eerie
Cytat
Ja do tego uzywam Panther

Zainstalowałem Panther Composer'em dla mojego projektu i mam problem. Gdy uruchamiam test w "Wierszu polecania"...

Kod
php vendor/bin/phpunit tests/

...wyświetla mi się "General error":

Cytat
'.' is not recognized as an internal or external command

Znalazłem na stackoverflow.com, aby użyć WSL2 "Windows Subsystem For Linux". Z poziomu okna MSYS2 uruchamiam test w PHPUnit...

Kod
/c/xampp/php/php ./vendor/bin/phpunit tests/

...i wywala mi ten sam błąd.

W czym tkwi problem? Bo użycie Bash'a w WSL nie pomaga i jakiś tam skrypt dalej wyrzuca mi ten błąd pod Windows... :| smile.gif

PS W pliku mojego testu MainPageControllerTest.php w linii 21 - wskazywanej jako problematyczna - jest kod:

Kod
$client->request('GET', '/');

...i to on przyczynia się do wyświetlenia błędu. Jak go zakomentuję/usunę, to błędu nie ma. Dziwne. smile.gif

PS2 Żeby było łatwiej, podaję wersje roboczą tej klasy:

Kod
<?php

declare(strict_types=1);

namespace App\Tests\Controller;

use Symfony\Component\Panther\PantherTestCase;

class MainPageControllerTest extends PantherTestCase
{
    public function testMainPageAndClickPolishLanguageLink(): void
    {
        // your app is automatically started using the built-in web server
        $client = static::createPantherClient();
        $client->request('GET', '/');
/*
        $client = self::createPantherClient([
            'hostname' => '127.0.0.9', // defaults to 127.0.0.1
            'port' => 8080, // defaults to 9080
        ]);
        $client->request('GET', '/');
*/
        // use any PHPUnit assertion, including the ones provided by Symfony...
        //$this->assertPageTitleContains('PHP Framework from EEQSOFT');

        [...]
    }
}


PS3 Wydaje mi się, że sprawę rozwiąże użycie Docker'a. Jutro sprawdzę. wink.gif
nospor
Cytat
ydaje mi się, że sprawę rozwiąże użycie Docker'a. Jutro sprawdzę.

No ja uzywam tego wlasnie na linux z dockerem i nie mialem takich problemow wiec trudno powiedziec co jest zle u ciebie. Daj znac jak ci poszlo z tym docker
eerie
Cytat
Daj znac jak ci poszlo z tym docker


Odpaliłem projekt na Docker'ze. Użyłem Composer'a, aby zainstalować biblioteki. I niby wszystko jest zainstalowane, włącznie z dbrekelmans/bdi, a przy uruchamianiu testu phpunit wyświetla mi się komunikat:

Cytat
Symfony\Component\Panther\Exception\RuntimeException: "geckodriver" binary not found. Install it using the package manager of your operating system or by running "composer require --dev dbrekelmans/dbi && vendor/bin/dbi detect drivers"


Jak wpisuję "composer require --dev dbrekelmans/db" czy "composer update dbrekelmans/db", to wyświetla się info, że jest zainstalowany. Natomiast polecenie "./vendor/bin/dbi detect drivers" nic nie zwraca. Pod Windows miałem jakiś komunikat, że jest ok. A tu nic... Coś jest nie tak, a ja jestem trochę "zielonkawy" z Linux'a. wink.gif
nospor
Nie wiem, nigdy nie widzialem takich problemow. Podam ci co ja mam, moze ci to pomoze:

docker-compose.yml

version: '3'

services:
php8:
container_name: php8
build:
context: .
dockerfile: docker/images/php/Dockerfile
volumes:
- ./appcode:/var/www/html
- ./docker/images/php/conf/dev-php.ini:/usr/local/etc/php/conf.d/php-settings.ini

web:
container_name: web
ports:
- "8083:80"
build:
context: .
dockerfile: docker/images/nginx/Dockerfile
volumes:
- ./appcode:/var/www/html
- ./docker/images/nginx/conf:/tmp/nginx


PHP Dockerfile:
FROM php:8.4.1-fpm-alpine3.21

# Set the system locale and time
ENV TZ=Europe/London LANG=en_GB.UTF-8 LANGUAGE=en_GB.UTF-8 LC_ALL=en_GB.UTF-8 LC_CTYPE=en_GB.UTF-8
RUN apk update \
&& apk upgrade \
&& apk add --no-cache \
tzdata \
musl-locales \
musl-locales-lang \
&& ln -snf /usr/share/zoneinfo/$TZ /etc/localtime \
&& echo $TZ > /etc/timezone

# Install additional php extensions
ADD https://github.com/mlocati/docker-php-exten...-php-extensions /usr/local/bin/
RUN chmod +x /usr/local/bin/install-php-extensions \
&& sync \
&& install-php-extensions \
bcmath \
gd \
intl \
pcntl \
pdo_mysql \
xdebug \
zip

# Chromium and ChromeDriver
ENV PANTHER_NO_SANDBOX=1
ENV PANTHER_CHROME_ARGUMENTS='--disable-dev-shm-usage'
RUN apk add --no-cache chromium chromium-chromedriver

# Install Composer
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer

# Install php-cs-fixer
RUN curl -L https://cs.symfony.com/download/php-cs-fixer-v3.phar -o php-cs-fixer \
&& chmod a+x php-cs-fixer \
&& mv php-cs-fixer /usr/local/bin/php-cs-fixer

# Clean up temporary files to keep image small
RUN rm -rf /var/cache/apk/* /var/tmp/* /tmp/*

# Configure the run.sh script to be executed
COPY docker/images/php/run.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/run.sh;

CMD ["/usr/local/bin/run.sh"]

composer.json
{
"type": "project",
"license": "proprietary",
"minimum-stability": "stable",
"prefer-stable": true,
"require": {
"php": ">=8.4",
"ext-ctype": "*",
"ext-iconv": "*",
"doctrine/annotations": "^1.0",
"doctrine/doctrine-bundle": "^2.10",
"doctrine/doctrine-migrations-bundle": "^3.2",
"doctrine/orm": "^2.12",
"guzzlehttp/guzzle": "^7.3",
"phpdocumentor/reflection-docblock": "^5.3",
"phpoffice/phpspreadsheet": "^1.29",
"phpstan/phpdoc-parser": "^1.6",
"symfony/asset": "7.2.*",
"symfony/console": "7.2.*",
"symfony/doctrine-messenger": "7.2.*",
"symfony/dotenv": "7.2.*",
"symfony/expression-language": "7.2.*",
"symfony/flex": "^2",
"symfony/form": "7.2.*",
"symfony/framework-bundle": "7.2.*",
"symfony/http-client": "7.2.*",
"symfony/intl": "7.2.*",
"symfony/mailer": "7.2.*",
"symfony/mime": "7.2.*",
"symfony/monolog-bundle": "^3.0",
"symfony/notifier": "7.2.*",
"symfony/process": "7.2.*",
"symfony/property-access": "7.2.*",
"symfony/property-info": "7.2.*",
"symfony/proxy-manager-bridge": "6.4.*",
"symfony/runtime": "7.2.*",
"symfony/security-bundle": "7.2.*",
"symfony/serializer": "7.2.*",
"symfony/string": "7.2.*",
"symfony/translation": "7.2.*",
"symfony/twig-bundle": "7.2.*",
"symfony/validator": "7.2.*",
"symfony/web-link": "7.2.*",
"symfony/webpack-encore-bundle": "^2.1",
"symfony/yaml": "7.2.*",
"twig/extra-bundle": "^2.12|^3.0",
"twig/twig": "^2.12|^3.0"
},
"config": {
"allow-plugins": {
"composer/package-versions-deprecated": true,
"symfony/flex": true,
"symfony/runtime": true
},
"optimize-autoloader": true,
"preferred-install": {
"*": "dist"
},
"sort-packages": true
},
"autoload": {
"psr-4": {
"App\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"App\\Tests\\": "tests/"
}
},
"replace": {
"symfony/polyfill-ctype": "*",
"symfony/polyfill-iconv": "*",
"symfony/polyfill-php72": "*",
"symfony/polyfill-php73": "*",
"symfony/polyfill-php74": "*",
"symfony/polyfill-php80": "*",
"symfony/polyfill-php81": "*"
},
"scripts": {
"auto-scripts": {
"cache:clear": "symfony-cmd",
"assets:install %PUBLIC_DIR%": "symfony-cmd"
},
"post-install-cmd": [
"@auto-scripts"
],
"post-update-cmd": [
"@auto-scripts"
]
},
"conflict": {
"symfony/symfony": "*"
},
"extra": {
"symfony": {
"allow-contrib": false,
"require": "7.2.*"
}
},
"require-dev": {
"dbrekelmans/bdi": "^1.0",
"phpunit/phpunit": "^9.5",
"symfony/browser-kit": "7.2.*",
"symfony/css-selector": "7.2.*",
"symfony/debug-bundle": "7.2.*",
"symfony/maker-bundle": "^1.0",
"symfony/panther": "^2.0",
"symfony/phpunit-bridge": "^7.2",
"symfony/stopwatch": "7.2.*",
"symfony/web-profiler-bundle": "7.2.*"
}
}

A tak odpalam testy z panther (e2e - tam trzymam testy dla panther)
docker exec -it --user=1000 php8 /bin/sh -c "php -d xdebug.mode=coverage bin/phpunit --testsuite e2e
eerie
Udało mi się rozwiązać ten problem. Wystarczyło dodać "geckodriver" [firefox'a] w Dockerfile:

Kod
[...]

RUN apt-get update                             \
&& apt-get install -y --no-install-recommends \
    ca-certificates curl firefox-esr           \
&& rm -fr /var/lib/apt/lists/*                \
&& curl -L https://github.com/mozilla/geckodriver/releases/download/v0.30.0/geckodriver-v0.30.0-linux64.tar.gz | tar xz -C /usr/local/bin \
&& apt-get purge -y ca-certificates curl

[...]


Niestety nie wiem, dlaczego test dla mojej klasy MainPageControllerTest pokazuje błąd, iż metoda assertPageTitleContains() jest niezdefiniowana.
nospor
No bo nie ma tej metody?
eerie
Co mam zrobić, aby móc użyć tej metody? W dokumentacji Panther'a napisali, że można użyć dowolnej "PHPUnit Assertion", ale u mnie nie są dostępne... A klasa TestCase jest abstrakcyjna. Nie mogę utworzyć obiektu klasy TestCase, a przy tym - o ile dobrze pamiętam - PHP pozwala rozszerzać maksymalnie jedną klasę. Da się jakoś użyć "extends" dla klas "PantherTestCase" i "TestCase" jednocześnie? A może brakuje mi jakichś bibliotek i dlatego nie działa? Jestem w kłopocie... wink.gif smile.gif
nospor
Sam sobie komplikujesz rzeczy. Poprostu zobacz co ma Panther. Jesli chcesz sprawdzac tytul, to masz przeciez:

self::assertPageTitleSame
eerie
Udało się... Działa! Napisałem taki test. Ma to sens? smile.gif

Kod
<?php

declare(strict_types=1);

namespace App\Tests\Controller;

use Symfony\Component\BrowserKit\HttpBrowser;
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\Panther\PantherTestCase;

class MainPageControllerTest extends PantherTestCase
{
    public function testMainPageTitleAndForm(): void
    {
        $browser = new HttpBrowser(HttpClient::create());

        $browser->request('GET', '/');
        $response = $browser->getResponse();

        preg_match(
            '/<title>(PHP Framework from EEQSOFT)<\/title>/',
            (string) $response,
            $matches
        );

        $this->assertEquals($matches[1], 'PHP Framework from EEQSOFT');

        $browser->clickLink('Polish');
        $response = $browser->getResponse();

        preg_match(
            '/<title>(PHP Framework od EEQSOFT)<\/title>/',
            (string) $response,
            $matches
        );

        $this->assertEquals($matches[1], 'PHP Framework od EEQSOFT');

        $browser->clickLink('Angielski');
        $browser->submitForm('Confirm', ['name' => 'Robert']);
        $response = $browser->getResponse();

        preg_match(
            '/(Welcome, Robert!)/',
            (string) $response,
            $matches
        );

        $this->assertEquals($matches[1], 'Welcome, Robert!');
    }
}
nospor
Nie bardzo rozumiem po co uzywasz regexp i szukasz po calym response... Po to uzywasz Panther by sie nie bawic w takie rzeczy

Twoje Witaj Robert pewnie jest w jakim div o jakiejs klasie. Robisz wiec

$this->assertSelectorTextContains('#twojDIV', 'Witaj Robert');
eerie
Cytat
$this->assertSelectorTextContains('#twojDIV', 'Witaj Robert');

Metoda jest niezdefiniowana... Nie mogę jej użyć.

Kod
        preg_match(
            '/<p class="ok">\r\n    (Welcome, Robert!)<br>/',
            (string) $response,
            $matches
        );

        $this->assertEquals($matches[1], 'Welcome, Robert!');

Takie coś działa, ale nie wygląda to zbyt profesjonalnie. :]

PS Zdaje się, że w PHPUnit 9.6 część dawnych metod została usunięta, ale można użyć takie coś:

Kod
<?php

declare(strict_types=1);

namespace App\Tests\Controller;

use Symfony\Component\BrowserKit\HttpBrowser;
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\Panther\PantherTestCase;

class MainPageControllerTest extends PantherTestCase
{
    public function testMainPageTitleAndForm(): void
    {
        $browser = new HttpBrowser(HttpClient::create());

        $browser->request('GET', '/');
        $response = $browser->getResponse();

        $this->assertStringContainsString(
            '<title>PHP Framework from EEQSOFT</title>',
            (string) $response
        );

        $browser->clickLink('Polish');
        $response = $browser->getResponse();

        $this->assertStringContainsString(
            '<title>PHP Framework od EEQSOFT</title>',
            (string) $response
        );

        $browser->clickLink('Angielski');
        $browser->submitForm('Confirm', ['name' => 'Robert']);
        $response = $browser->getResponse();

        $this->assertStringContainsString(
            'Welcome, Robert!',
            (string) $response
        );
    }
}
nospor
Cytat
Metoda jest niezdefiniowana... Nie mogę jej użyć.

To uzyj innej, podobnej, moze zmienili nazwe troche czy cos takiego
eerie
Cytat(nospor @ 24.03.2025, 10:03:04 ) *
To uzyj innej, podobnej, moze zmienili nazwe troche czy cos takiego

Przeszukałem wszystkie assertions w dokumentacji i nic podobnego nie znalazłem. wink.gif smile.gif

PS Wygląda mi, jakby pousuwali sporo metod i ostro to uprościli.
nospor
Ale czemu ty tego szukasz w phpunit?

To jest w Panther a nie w phpunit
https://github.com/symfony/panther/blob/mai...rtionsTrait.php
eerie
Przeanalizowałem klasę PantherTestCase dla v2.0.0 mojej instalacji Panther'a. Bez instalacji FrameworkBundle Symfony klasa WebTestCase jest niedostępna i klasa abstrakcyjna PantherTestCase rozszerza TestCase PHPUnit oraz używa PantherTestCaseTrait, a nie WebTestAssertionsTrait. Przez to metody takie jak assertSelectorTextContains() są niedostępne. Nie wiem, czy powiniennem zainstalować specjalnie FrameworkBundle... I czy to będzie działać? Prościej będzie użyć metody assertStringContainsString() z PHPUnit, jak ja to zrobiłem w moim teście MainPageControllerTest.

Metoda assertSelectorTextContains() i tak używa assertStringContainsString():

Kod
    public static function assertSelectorTextContains(string $selector, string $text, string $message = ''): void
    {
        self::assertStringContainsString($text, self::getText($selector), $message);
    }

Dlatego można spokojnie użyć tej metody... Tak myślę...

Kod
        $this->assertStringContainsString(
            '<title>PHP Framework from EEQSOFT</title>',
            (string) $response
        );
nospor
Aha. To by wyjasnialo pare rzeczy biggrin.gif
eerie
Napisałem kilka testów dla klas source'a. Nie da się zbytnio przetestować Controller'ów i Service'ów, przynajmniej ja nie wiem jak, więc użyłem HttpBrowser. Przetestowałem też Repository, ale moje testy ingerują w dane bazy danych. Nie wiem, czy tak można? Przetestowałem też Validator'a z użyciem Mock'a dla klasy CsrfToken, która zapisuje token w sesji... Czy macie uwagi? Popełniłem rażące błędy, które wypada poprawić?

PS Wypadałoby chyba przetestować jeszcze Core'a? Czyli rdzenne klasy mojego framework'a... smile.gif
To jest wersja lo-fi głównej zawartości. Aby zobaczyć pełną wersję z większą zawartością, obrazkami i formatowaniem proszę kliknij tutaj.
Invision Power Board © 2001-2025 Invision Power Services, Inc.