PHPUnit и Unit и Functional тесты

Установка и настройка PHPUnit. Написание Unit-тестов. Создание инфраструктуры функциональных тестов для API. Анализ тестового покрытия.

  • 00:00:34 Проблемы логики
  • 00:03:14 Фреймворк PHPUnit
  • 00:06:40 Файл конфигурации
  • 00:13:17 Переменные окружения для тестов
  • 00:15:04 Папка tests
  • 00:15:33 Какие тесты нужны
  • 00:21:43 Первый Unit-тест
  • 00:26:29 Команда запуска
  • 00:29:31 Параметризованные тесты
  • 00:32:56 Тестирование контроллеров
  • 00:33:43 Команда check
  • 00:34:11 Функциональный тест
  • 00:40:30 Вынос повторяющегося кода
  • 00:43:20 Что нам помогло
  • 00:45:56 Логирование ошибок в тестах
  • 00:49:09 Разделение на Test Suite
  • 00:52:43 Анализ тестового покрытия кода
  • 00:59:12 Аннотация covers
  • 01:01:05 Покрытие функциональными тестами
  • 01:04:06 Форматы отчёта
  • 01:05:41 Очистка мусора при инициализации
  • 01:07:44 Обзор результата
Скрытый контент (код, слайды, ...) для подписчиков. Открыть →
Дмитрий Елисеев
elisdn.ru
Комментарии (57)
Александр Кулик

Большая просьба. Добавьте пожалуйста в .gitignore папки .idea.

Ответить
Дмитрий Елисеев

У всех редакторы могут быть разные. В Git для этого можно указать глобальный игнор:

git config --global core.excludesfile ~/.gitignore

и добавлять папки своих редакторов туда:

echo '.idea' >> ~/.gitignore
Ответить
Александр Кулик

Спасибо

Ответить
Дмитрий

Большое спасибо!

Ответить
Arunas

спасибо за замечательный урок.

Ответить
Arunas

возможно ли тест: множественный запрос (напр. 100 раз в сек) от одного и того же IP, т.е. что-бы обнаружить атаку? как организована такая защита на Slim? как лучше организовать защиту? Будет о защите на уроках?

Ответить
Дмитрий Елисеев

Да, добавим Rate Limiter.

Ответить
Arunas

Срасибо

Ответить
Ruslan

Странно, чтоб сайт на PHP сам боролся с DDOS. Просто поставьте ограничение в фаерволе ведь он наверное для этого. Да и вообще по ситуации, есть различные ситуации - разные решения.

Ответить
Алексей

Согласен. Если по серьезному начнут ddos-ить, то в первую очередь ляжет сервер. До web приложения не дойдет

Ответить
Ruslan

rate limit можно наложить на любой порт и на 80, 443 тоже.

Ответить
Denis

В чем разница между PhpUnit и Codeception? Почему во всех уроках используете PhpUnit?

Ответить
Дмитрий Елисеев

Codeception построен поверх PHPUnit, дополняя его своими модулями для готовых фреймворков и позволяя делать функциональные и приёмочные тесты в своём Cest-стиле.

Он с модулями удобен как быстрое решение для готовых фреймворков, но неудобен для кастомных проектов с вынесенным фронтендом.

Поэтому мы возьмём оригинальный PHPUnit для тестов API, отдельный JS-фреймворк для тестов JS и отдельный BDD-фреймворк для приёмочных тестов всей системы.

Ответить
Юрий

Дмитрий, спасибо за урок, все замечательно, а был ли у Вас опыт использования мутационного тестирования? Если да, то что думаете по этому поводу? Спасибо.

Ответить
Дмитрий Елисеев

Особо не использовал из-за медленной работы, но вещь полезная.

Ответить
Sergei

Дмитрий, подскажите любезно, что с правами не так. Винда =/ Докер и консоль от администратора запущено. Докер ап работет исправно, сайт поднимается. А вот make init. Ерунда какая то, есть идеи? Спасибо.

docker-compose run --rm api-php-cli composer install
Loading composer repositories with package information
Nothing to install or update
Generating autoload files
docker run --rm -v /cygdrive/c/projects/demo-auction/api:/app -w /app alpine chmod 777 var
chmod: var: No such file or directory
make: *** [Makefile:27: api-permissions] Error 1
Ответить
Дмитрий Елисеев

А что выводит команда ls ?

docker run --rm -v ${PWD}/api:/app -w /app alpine ls -l
Ответить
Sergei
$ docker run --rm -v ${PWD}/api:/app -w /app alpine ls -l
total 0
Ответить
Дмитрий Елисеев

Docker-демон не видит виртуальную папку /cygdrive, которая. существует только в консоли Git.

Установите Make for Windows и запускайте команды в консоли PowerShell.

Ответить
Sergei

Установил эту утилиту, выполняю её из powerShell:

ocramius/package-versions: Generating version class...
ocramius/package-versions: ...done generating version class
docker run --rm -v /api:/app -w /app alpine chmod 777 var
chmod: var: No such file or directory
make: *** [api-permissions] Fehler 1
PS C:\Users\Symfony\Projects\demo-auction>

Странно, а остальные команды работают

Ответить
Дмитрий Елисеев

В -v /api:/app адрес должен подставляться из ${PWD} абсолютный C:\Users\...

Ответить
Sergei

Странное поведение, потому что PWD в консоли работает:

PS C:\Users\Symfony\Projects\demo-auction> echo ${PWD}

Path
----
C:\Users\Symfony\Projects\demo-auction

Прописал прямо путь, ошибок не выбросило.

Nothing to install or update
Generating autoload files
ocramius/package-versions: Generating version class...
ocramius/package-versions: ...done generating version class
docker run --rm -v C:/Users/Symfony/Projects/demo-auction/api:/app -w /app alpine chmod 777 var
PS C:\Users\Symfony\Projects\demo-auction>
Ответить
Дмитрий Елисеев

Значит та консоль подменяет $PWD на свой /cygdrive

Ответить
Sergei

Скорее всего. Но конкретно предыдущий лог из консоли это из powershell и make утилиты. и PWD в повершелле работает корректно.

PS C:\Users\Symfony\Projects\demo-auction> echo ${PWD}

Path
----
C:\Users\Symfony\Projects\demo-auction

Но ошибка все равно сыпется. Ерунда какая то :(

Ответить
KonTuzh

Я на винде прописал путь так и все работает: docker run --rm -v //${PWD}/api://app -w /app alpine chmod 777 var

Ответить
Sergei

Неа, с двойными слэшами тоже не работает :( Только абсолютный путь

Ответить
Ruslan

У меня таже проблема, ломается на этом месте. Подскажите, что мы хотим добится в этом месте?

api-permissions:
	docker run --rm -v ${PWD}/api:/app -w /app alpine chmod 777 var

Если только установить права на var. Но если ,я не запущу эту команду, то получу теже права:

PS C:\projects\Docker\eliseev\demo-auction> docker run --rm -v ${PWD}/api:/app -w /app alpine ls -l
total 152
drwxrwxrwx    1 root     root          4096 Jan 23 21:20 bin
-rwxrwxrwx    1 root     root          1037 Feb  4 09:57 composer.json
-rwxrwxrwx    1 root     root        144228 Feb  4 09:57 composer.lock
drwxrwxrwx    1 root     root          4096 Jan 23 21:20 config
drwxrwxrwx    1 root     root          4096 Jan 21 15:16 docker
-rwxrwxrwx    1 root     root           553 Feb  4 09:57 phpcs.xml
-rwxrwxrwx    1 root     root          1011 Feb  4 09:57 phpunit.xml
-rwxrwxrwx    1 root     root           670 Feb  4 11:05 psalm.xml
drwxrwxrwx    1 root     root          4096 Jan 21 15:16 public
drwxrwxrwx    1 root     root          4096 Jan 21 15:16 src
drwxrwxrwx    1 root     root          4096 Feb  4 11:05 tests
drwxrwxrwx    1 root     root          4096 Jan 23 17:52 var
drwxrwxrwx    1 root     root          4096 Feb  4 12:11 vendor

Можно ли в данном случае обойтись без абсолютного пути? ${PWD}/api

Ответить
Дмитрий Елисеев

Да, только устанавливаем права. Для docker-compose нужны относительные пути, а для docker требуются только абсолютные.

Ответить
elmut

docker run --rm -v $$PWD/api:/app -w /app alpine ls -l

попробуйте так.

echo $$PWD

Ответить
Ruslan

Дошел до 27 минуты, у меня не видны тесты:

PS C:\projects\Docker\eliseev\react_slim> docker-compose run --rm api-php-cli vendor/bin/phpunit
PHPUnit 8.5.2 by Sebastian Bergmann and contributors.

Runtime:       PHP 7.4.1 with Xdebug 2.9.1
Configuration: /app/phpunit.xml



Time: 1.3 seconds, Memory: 4.00 MB

No tests executed!
Ответить
Дмитрий Елисеев

А phpunit.xml настроен верно?

Ответить
Ruslan

Я в тупиковых ситуациях смотрю ваши комиты и беру от туда:

<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/8.5/phpunit.xsd"
         bootstrap="vendor/autoload.php"
         executionOrder="depends,defects"
         forceCoversAnnotation="true"
         beStrictAboutCoversAnnotation="true"
         beStrictAboutOutputDuringTests="true"
         beStrictAboutTodoAnnotatedTests="true"
         cacheResultFile="var/.phpunit.result.cache"
         verbose="true">
  <testsuites>
    <testsuite name="default">
      <directory suffix="Test.php">tests</directory>
    </testsuite>
  </testsuites>

  <filter>
    <whitelist processUncoveredFilesFromWhitelist="true">
      <directory suffix=".php">src</directory>
    </whitelist>
  </filter>

  <php>
    <env name="APP_ENV" value="test" force="true"/>
    <env name="APP_DEBUG" value="1" force="true"/>
  </php>
</phpunit>
Ответить
Алекс

попробуйте выполнить команду composer dump-autoload

Ответить
Rodion

Всем добрый вечер. У кого то зависает загрузка видео на скорости 1.25 или 1.5? C интернетом перебоя нет. Это не в первый раз.

Ответить
kashamamina

их не может быть, а если и случаются то зависят от Vimeo, так как он используется для размещения видео

Ответить
Дмитрий Елисеев

Зависания были раньше, когда использовался файловый хостинг. Сейчас после перехода на Vimeo проблем со скоростью нет.

Ответить
Александр

Дмитрий, я хотел сам поставить phpunit,

docker-compose run --rm api-php-cli composer require --dev phpunit/phpunit

не дало

Your requirements could not be resolved to an installable set of packages.
  Problem 1
    - Conclusion: don't install phpunit/phpunit 9.2.4
    - Conclusion: don't install phpunit/phpunit 9.2.3
    - Conclusion: don't install phpunit/phpunit 9.2.2
    - Conclusion: don't install phpunit/phpunit 9.2.1
    - Conclusion: remove sebastian/diff 3.0.2
    - Installation request for phpunit/phpunit ^9.2 -> satisfiable by phpunit/phpunit[9.2.0, 9.2.1, 9.2.2, 9.2.3, 9.2.4].
    - Conclusion: don't install sebastian/diff 3.0.2
    - phpunit/phpunit 9.2.0 requires sebastian/diff ^4.0 -> satisfiable by sebastian/diff[4.0.0, 4.0.1].
    - Can only install one of: sebastian/diff[4.0.0, 3.0.2].
    - Can only install one of: sebastian/diff[4.0.1, 3.0.2].
    - Installation request for sebastian/diff (locked at 3.0.2) -> satisfiable by sebastian/diff[3.0.2].

Installation failed, reverting ./composer.json to its original content.

Ладно , взял код из комита - Added JSON response test

make init

docker-compose run --rm api-php-cli vendor/bin/phpinput --help

и в итоге - /usr/local/bin/docker-php-entrypoint: exec: line 9: vendor/bin/phpinput: not found

что где пропустил не пойму подскажите ?

Ответить
Дмитрий Елисеев

Попробуйте удалить папку vendor и поставить через composer require снова. Если не получится, то сделайте сначала composer remove phpunit/phpunit, а потом снова requre.

Ответить
Александр

phpunit в вендоре есть при любом раскладе и переустановке. Но все равно при запуске от любого коммита и после переустановки при вводе команды

docker-compose run --rm api-php-cli vendor/bin/phpinput --help

ответ один

/usr/local/bin/docker-php-entrypoint: exec: line 9: vendor/bin/phpinput: not found
Ответить
Дмитрий Елисеев

Не phpinput, а phpunit

Ответить
Александр

ага точно, походу ночью лучше спать ))))

Ответить
Roman Korolov

Спасибо!

Ответить
Андрей

Добрый день, Дмитрий.

У меня phpunit 9.3.8 и psalm 3.14.2. Plalm выдает ошибку одинаковую для всех файлов Unit тестов, сами тесты работают нормально:

ERROR: PropertyNotSetInConstructor - src/Http/Test/Unit/EmptyResponseTest.php:10:7 - Property App\Http\Test\Unit\EmptyResponseTest::$backupStaticAttributes is not defined in constructor of App\Http\Test\Unit\EmptyResponseTest and in any methods called in the constructor (see https://psalm.dev/074)
class EmptyResponseTest extends TestCase

Подскажите в чем может быть проблема?

Ответить
Дмитрий Елисеев

Можно добавить глобальное подавление этой ошибки для всех тестов в psalm.yml:

<issueHandlers>
    <LessSpecificReturnType errorLevel="info" />

    <!-- PHPUnit -->
    <PropertyNotSetInConstructor>
        <errorLevel type="suppress">
            <directory name="tests" />
        </errorLevel>
    </PropertyNotSetInConstructor>
</issueHandlers>
Ответить
А

У меня phpunit 9 ругается на отсутствие @covers. Дописал к классу теста @covers JsonResponse и он перестал это делать.

Test\Unit\Http\JsonResponseTest::testObject This test does not have a @covers annotation but is expected to have one

Ответить
А

UPD: досмотрел видео до конца и директивы в конфиге

Ответить
Sam

У меня при запуску coverage тестов ругается

Configuration: /app/phpunit.xml Warning: XDEBUG_MODE=coverage or xdebug.mode=coverage has to be set

Чтобы пофиксить в Ubuntu переделал команду в следующую:

"test-unit-coverage": "XDEBUG_MODE=coverage phpunit --colors=always --testsuite=unit --coverage-html var/coverage",
Ответить
Антон

Было тоже самое, поменял в api/docker/development/php/conf.d/xdebug.ini xdebug.mode=debug на xdebug.mode=coverage. Лучше ничего не придумал пока)

Ответить
Дмитрий Елисеев

Лучше как раз передавать XDEBUG_MODE=coverage при выполнении, как в совете выше.

Мы сделали также в 44-ом эпизоде про новый Xdebug 3

Ответить
Антон

Отлично, спасибо!

Ответить
Иван

Здравствуйте! Немного не в тему, но подскажите пожалуйста, есть ли в планах скринкасны по Python? Также охватывающие тему деполоя, сборки, тестов, контейнеризация.

Ответить
Дмитрий Елисеев

Python в планах нет. А контейнеризация и деплой везде одинаковые.

Ответить
Рачков Роман

Здравствуйте. Подскажите такой момент: У меня на testMethod и на testNotFound валятся ошибки Error: Value of type bool is not callable при этом если их по отдельности по 1 запустить то они нормально проходят, я понимаю что это из за того что экземпляр приложения после первого теста создан, и при попытки создать его еще раз и валится эта ошибка. Только вот я не понимаю как ее обойти. версия slim 4.9 версия phpunit 9.5

Ответить
Рачков Роман

Вопрос отпал, у мена в файле config/app.php были прописаны require_once а не просто require

Ответить
Юлия Королева

Здравствуйте! При запуске

docker-compose run --rm api-php-cli composer test-coverage -- --testsuite=unit

ошибка

Fatal error: Uncaught Error: Class 'PHP_Token_CLASS' not found in /app/vendor/phpunit/php-token-stream/src/Stream.php:477

команда в Composer

"test-coverage": "phpunit --colors=always --coverage-html var/coverage"

С чем это может быть связано?

Ответить
Дмитрий Елисеев

Может проблема текущей верси PHPUnit.

Ответить
gfdgdf

здравствуйте, меня всегда мучает такой вопрос: как тестить сервисы с более менее сложным функционалом? Или все надо вынести в отдельные функции так, чтобы не было сложной логики и тогда не будет необходимости тестить основной сервис который оперирует маленькими функциями?

пример такого сервиса:

public function getSynched(int $customerId): Customer
    {
        $customer = $this->getUnSynched($customerId);

        try {
            $results = $this->mwService->request('post', '/billing/api/customer/info/', [
                'customer_id' => $customerId
            ]);
        } catch (MwException $e) {
            if ($e->getCode() === 7001) {
                CustomerRepository::delete($customer);
            }
            throw $e;
        }

        $customer->setAbonement($results['accounts'][0]['abonement']);
        $customer->updateBalanceFromSom($results['balance']);
        $customer->sync();
        $customer->save();

        foreach ($results['subscriptions'] as $remoteSubscription) {

            if ($remoteSubscription['end_date'] === null) {
                continue;
            }

            if (!$tariff = TariffRepository::findById($remoteSubscription['tariff_id'])) {
                $tariff = Tariff::make($remoteSubscription['tariff_id'], $remoteSubscription['price']);
                $tariff->save();
            }

            /** @var Subscription $localSubscription */
            $localSubscription = SubscriptionRepository::findByCustomerAndTariffId($customer, $remoteSubscription['tariff_id']);

            if ($remoteSubscription['is_periodical'] == 0) {
                if (!$localSubscription) {
                    $expiredAt = Carbon::parse($remoteSubscription['end_date']);
                    $localSubscription = $this->subscriptionService->create($customer, $tariff, $expiredAt);
                }

                $localSubscription->cancel();
                $localSubscription->save();

                continue;
            }

            if ($localSubscription) {
                $localSubscription->updatePriceFromSom($remoteSubscription['price']);
                if (isset($remoteSubscription['end_date']) && $localSubscription->isSyncExpired()) {
                    $expiredAt = Carbon::parse($remoteSubscription['end_date']);
                    $localSubscription->renew($expiredAt);
                }
            } else {
                if (isset($remoteSubscription['end_date'])) {
                    $expiredAt = Carbon::parse($remoteSubscription['end_date']);
                    $localSubscription = $this->subscriptionService->create($customer, $tariff, $expiredAt);
                } else {
                    $localSubscription = $this->subscriptionService->create($customer, $tariff);
                }
            }

            $localSubscription->save();
        }

        $customer->subscriptions()
            ->whereNotIn('tariff_id', array_column($results['subscriptions'], 'tariff_id'))
            ->delete();

        return $customer;
    }
Ответить
Зарегистрируйтесь или войдите чтобы оставить комментарий

Или войти через:

Yandex
MailRu
GitHub
Google