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
Комментарии (60)
Александр Кулик
2020-01-11 21:30

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

Ответить
Дмитрий Елисеев
2020-01-12 05:59

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

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

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

echo '.idea' >> ~/.gitignore
Ответить
Александр Кулик
2020-01-15 05:49

Спасибо

Ответить
Дмитрий
2020-03-13 14:54

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

Ответить
Arunas
2020-01-12 14:22

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

Ответить
Arunas
2020-01-12 14:22

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

Ответить
Дмитрий Елисеев
2020-01-13 09:48

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

Ответить
Arunas
2020-01-13 12:14

Срасибо

Ответить
Ruslan
2020-01-13 21:59

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

Ответить
Алексей
2020-01-15 14:54

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

Ответить
Ruslan
2020-01-15 15:29

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

Ответить
Denis
2020-01-12 20:45

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

Ответить
Дмитрий Елисеев
2020-01-13 10:32

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

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

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

Ответить
Юрий
2020-01-15 11:38

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

Ответить
Дмитрий Елисеев
2020-01-17 14:53

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

Ответить
Sergei
2020-01-27 18:07

Дмитрий, подскажите любезно, что с правами не так. Винда =/ Докер и консоль от администратора запущено. Докер ап работет исправно, сайт поднимается. А вот 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
Ответить
Дмитрий Елисеев
2020-01-27 20:27

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

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

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

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

Ответить
Sergei
2020-01-27 21:47

Установил эту утилиту, выполняю её из 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>

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

Ответить
Дмитрий Елисеев
2020-01-28 06:58

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

Ответить
Sergei
2020-01-28 21:21

Странное поведение, потому что 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>
Ответить
Дмитрий Елисеев
2020-01-31 07:32

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

Ответить
Sergei
2020-01-31 14:44

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

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

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

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

Ответить
KonTuzh
2020-02-11 20:06

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

Ответить
Sergei
2020-04-19 22:06

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

Ответить
Ruslan
2020-02-04 12:30

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

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

Ответить
Дмитрий Елисеев
2020-02-05 12:20

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

Ответить
elmut
2020-03-19 07:19

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

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

echo $$PWD

Ответить
Ruslan
2020-02-04 12:41

Дошел до 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!
Ответить
Дмитрий Елисеев
2020-02-05 12:20

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

Ответить
Ruslan
2020-02-05 16:33

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

<?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>
Ответить
Алекс
2020-02-05 19:35

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

Ответить
Rodion
2020-02-05 20:16

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

Ответить
kashamamina
2020-12-18 22:44

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

Ответить
Дмитрий Елисеев
2020-12-19 10:12

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

Ответить
Александр
2020-06-21 18:27

Дмитрий, я хотел сам поставить 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

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

Ответить
Дмитрий Елисеев
2020-06-23 10:15

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

Ответить
Александр
2020-06-25 21:27

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
Ответить
Дмитрий Елисеев
2020-06-28 17:05

Не phpinput, а phpunit

Ответить
Александр
2020-06-29 06:16

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

Ответить
Roman Korolov
2020-07-04 12:00

Спасибо!

Ответить
Андрей
2020-09-03 06:58

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

У меня 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

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

Ответить
Дмитрий Елисеев
2020-09-03 09:31

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

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

    <!-- PHPUnit -->
    <PropertyNotSetInConstructor>
        <errorLevel type="suppress">
            <directory name="tests" />
        </errorLevel>
    </PropertyNotSetInConstructor>
</issueHandlers>
Ответить
А
2021-01-10 12:57

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

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

Ответить
А
2021-01-10 15:30

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

Ответить
Sam
2021-09-07 16:06

У меня при запуску 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",
Ответить
Антон
2021-11-07 15:48

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

Ответить
Дмитрий Елисеев
2021-11-11 10:09

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

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

Ответить
Антон
2021-11-13 10:03

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

Ответить
Иван
2021-10-02 10:03

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

Ответить
Дмитрий Елисеев
2021-10-05 13:17

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

Ответить
Рачков Роман
2022-03-06 01:46

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

Ответить
Рачков Роман
2022-03-06 02:16

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

Ответить
Юлия Королева
2022-06-05 12:57

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

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"

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

Ответить
Дмитрий Елисеев
2022-06-29 06:52

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

Ответить
gfdgdf
2024-08-14 17:24

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

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

    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;
    }
Ответить
Дмитрий Елисеев
2024-11-23 08:37

Да, всё так. Либо тестировать большой сервис только интеграционными тестами, либо разбить на отдельные подменяемые моками и стабами части, которые можно будет протестировать отдельными юнит-тестами.

Ответить
Только у меня видео зацикливается на одном месте?
2025-04-03 11:53

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

Ответить
Только у меня видео зацикливается на одном месте?
2025-06-21 09:56

Напишу что все исправлено!

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

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

Yandex
MailRu
GitHub
Google