Очередное занятие курса “Otus PHP Professional“ будет посвящено практике написания тестов на CodeCeption. Рассмотрим применение разноуровневых тестов на практике. Цель — научиться понимать, уметь и использовать разные способы тестирования. В результате научимся составлять тест-кейсы; преобразовывать их в код; следить за покрытием кода тестами.

Преподаватели: Олег Мельник, Tech lead in Proxify , Михаил Каморин

Дата: 20.04.2022

/**/

(warning) Идея: нужно повторить все те действия что делаются в лекции на своем домашнем ноутбуке, параллельно с лектором (см. исходники в ссылках ниже, раздел “Доп. материалы“); см. инструкцию по поднятию проекта — в файле SCRIPT.MD

Тезисы из лекции

Карта курса “PHP Professional“

Изучаемый блок

Цели вебинара

Codeception

Codeception: модули

Webdriver: варианты работы

Практика тестирования

Инструкция из SCRIPT.MD

* Распаковываем архив `target.zip`, заходим в каталог `target`
* Устанавливаем зависимости командой `composer install`
* Запускаем тестируемый проект `docker-compose up`
* Заходим на адрес `http://localhost:7777/api/doc`, авторизуемся `admin` / `my_pass` и видим документацию API
* Заходим в контейнер командой `docker exec -it php sh` и выполняем команду
`php bin/console doctrine:migrations:migrate` для того, чтобы накатить миграции на БД
* Выходим из контейнера и возвращаемся обратно в корневой каталог
* Устанавливаем необходимые пакеты командой
    ```shell script
    composer require --dev codeception/codeception codeception/module-phpbrowser codeception/module-rest codeception/module-asserts
    ```
* Создаём файл `codeception.yml`
    ```yaml
    namespace: Tests
    paths:
        tests: tests
        output: tests/_output
        data: tests/_data
        support: tests/_support
        envs: tests/_envs
    actor_suffix: Tester
    extensions:
        enabled:
            - CodeceptionExtensionRunFailed
    ```
* Создаём файл `tests/acceptance.suite.yml`
    ```yaml
    actor: AcceptanceTester
    modules:
        enabled:
            - REST:
                  url: http://localhost:7777
                  depends: PhpBrowser
                  part: Json
            - Asserts
    ```
* Собираем актор командой `vendor/bin/codecept build`
* В файле `composer.json` исправляем секцию `autoload-dev`
    ```json
    "autoload-dev": {
        "psr-4": {
            "Tests\": "tests/acceptance"
        }
    },
    ```
* Создаём файл `tests/acceptance/UserCest.php`
    ```php
    <?php
    
    namespace Tests;
    
    use CodeceptionUtilHttpCode;
    
    class UserCest
    {
        public function testAddUserUnauthorized(AcceptanceTester $I): void
        {
            $I->sendPost('/api/v1/user', ['login' => 'Terry Pratchett']);
            $I->canSeeResponseCodeIs(HttpCode::UNAUTHORIZED);
        }
    }
    ```
* Запускаем тесты командой `vendor/bin/codecept run`
* Добавляем в класс `AppTestsAcceptanceTester` новые методы:
    ```php
   
    public function amAdmin(): void
    {
        $this->amHttpAuthenticated('admin', 'my_pass');
    }
    
    public function amUser(): void
    {
        $this->amHttpAuthenticated('user', 'other_pass');
    }

    public function canSeeForbiddenResponse(): void
    {
        $this->canSeeResponseContainsJson(['message' => 'Access denied']);
        $this->canSeeResponseCodeIs(HttpCode::FORBIDDEN);
    }

    public function canSeeBadRequestResponse(): void
    {
        $this->canSeeResponseContainsJson(['success' => false]);
        $this->canSeeResponseMatchesJsonType(['success' => 'boolean']);
        $this->canSeeResponseCodeIs(HttpCode::BAD_REQUEST);
    }
    ```
* В классе `TestsUserCest` добавляем новые тесты:
    ```php
    public function testAddUserAsUser(AcceptanceTester $I): void
    {
        $I->amUser();
        $I->sendPost('/api/v1/user', ['login' => 'Terry Pratchett']);
        $I->canSeeForbiddenResponse();
    }

    public function testAddUserAsAdmin(AcceptanceTester $I): void
    {
        $I->amAdmin();
        $I->sendPost('/api/v1/user', ['login' => 'Terry Pratchett']);
        $I->canSeeResponseContainsJson(['success' => true]);
        $I->canSeeResponseMatchesJsonType(['success' => 'boolean', 'user_id' => 'integer:>0']);
        $I->canSeeResponseCodeIs(HttpCode::OK);
    }

    public function testAddUserBadRequest(AcceptanceTester $I): void
    {
        $I->amAdmin();
        $I->sendPost('/api/v1/user');
        $I->canSeeBadRequestResponse();
    }
    ```
* Снова запускаем тесты командой `vendor/bin/codecept run`
* Добавляем в класс `AppTestsAcceptanceTester` новый метод:
    ```php
    public function haveUser(string $login): int
    {
        $this->amAdmin();
        $this->sendPost('/api/v1/user', ['login' => $login]);
        return $this->grabDataFromResponseByJsonPath('user_id')[0];
    }
    ```
* Создаём файл `tests/acceptance/SubscriptionCest.php`
    ```php
    <?php
    
    namespace Tests;
    
    use CodeceptionUtilHttpCode;
    
    class SubscriptionCest
    {
        public function testAddSubscriptionUnauthorized(AcceptanceTester $I): void
        {
            $I->sendPost('/api/v1/subscription', ['authorId' => 1, 'followerId' => 2]);
            $I->canSeeResponseCodeIs(HttpCode::UNAUTHORIZED);
        }
    
        public function testAddSubscriptionAsAdmin(AcceptanceTester $I): void
        {
            $authorId = $I->haveUser('Lewis Carroll');
            $followerId = $I->haveUser('Follower #1');
            $I->amAdmin();
            $I->sendPost('/api/v1/subscription', ['authorId' => $authorId, 'followerId' => $followerId]);
            $I->canSeeForbiddenResponse();
        }
    
        public function testAddSubscriptionAsUser(AcceptanceTester $I): void
        {
            $authorId = $I->haveUser('Lewis Carroll');
            $followerId = $I->haveUser('Follower #1');
            $I->amUser();
            $I->sendPost('/api/v1/subscription', ['authorId' => $authorId, 'followerId' => $followerId]);
            $I->canSeeResponseCodeIs(200);
            $I->canSeeResponseContainsJson(['success' => true]);
            $I->canSeeResponseMatchesJsonType(['success' => 'boolean']);
        }

        public function testAddSubscriptionBadRequest(AcceptanceTester $I): void
        {
            $I->amUser();
            $I->sendPost('/api/v1/subscription');
            $I->canSeeBadRequestResponse();
        }
    }
    ```
* Запускаем тесты командой `vendor/bin/codecept run tests/acceptance/SubscriptionCest.php`, проверяем, что всё работает
* Добавляем в класс `AppTestsAcceptanceTester` новые методы:
    ```php
    public function haveSubscription(int $authorId, int $followerId): void
    {
        $this->amUser();
        $this->sendPost('/api/v1/subscription', ['authorId' => $authorId, 'followerId' => $followerId]);
    }

    /**
     * @return string[] $followerLogins
     */
    public function haveAuthorWithFollowers(string $login, array $followerLogins): int
    {
        $authorId = $this->haveUser($login);
        foreach ($followerLogins as $followerLogin) {
            $followerId = $this->haveUser($followerLogin);
            $this->haveSubscription($authorId, $followerId);
        }
        
        return $authorId;
    }

    public function seeCountItemsInResponseByJsonPath(string $path, int $count): void
    {
        $response = json_decode($this->grabResponse(), true, 512, JSON_THROW_ON_ERROR);
        $this->assertCount($count, $response[$path]);
    }
    ```
* Добавляем новые тесты в класс `TestsSubscriptionCest`
    ```php
    public function testListFollowersUnauthorized(AcceptanceTester $I): void
    {
        $I->sendGet('/api/v1/subscription/list-by-author?authorId=1');
        $I->canSeeResponseCodeIs(HttpCode::UNAUTHORIZED);
    }
    
    public function tesListFollowersAsAdmin(AcceptanceTester $I): void
    {
        $authorId = $I->haveAuthorWithFollowers('John Smith', ['Some follower']);
        $I->amAdmin();
        $I->sendGet("/api/v1/subscription/list-by-author?authorId=$authorId");
        $I->canSeeForbiddenResponse();
    }
    
    public function testListFollowersAsUser(AcceptanceTester $I): void
    {
        $follower1 = 'Jake';
        $follower2 = 'Harry';
        $follower3 = 'Susan';
        $follower4 = 'Richard';
        $follower5 = 'Michael';
        $author1Id = $I->haveAuthorWithFollowers('Joan Rowling', [$follower1, $follower2, $follower3]);
        $author2Id = $I->haveAuthorWithFollowers('Douglas Adams', [$follower4, $follower5]);
        $I->amUser();
        $I->sendGet("/api/v1/subscription/list-by-author?authorId=$author1Id");
        $I->canSeeResponseCodeIs(200);
        $I->canSeeResponseContainsJson(['login' => $follower1]);
        $I->canSeeResponseContainsJson(['login' => $follower2]);
        $I->canSeeResponseContainsJson(['login' => $follower3]);
        $I->seeCountItemsInResponseByJsonPath('followers', 3);
        $I->sendGet("/api/v1/subscription/list-by-author?authorId=$author2Id");
        $I->canSeeResponseCodeIs(200);
        $I->canSeeResponseContainsJson(['login' => $follower4]);
        $I->canSeeResponseContainsJson(['login' => $follower5]);
        $I->seeCountItemsInResponseByJsonPath('followers', 2);
    }
    ```
* Снова запускаем тесты командой `vendor/bin/codecept run tests/acceptance/SubscriptionCest.php`, проверяем, что всё работает
* Добавляем в класс `AppTestsAcceptanceTester` новые методы:
    ```php
    /**
     * @return string[] $authorLogins
     */
    public function haveFollowerWithAuthors(string $login, array $authorLogins): int
    {
        $followerId = $this->haveUser($login);
        foreach ($authorLogins as $authorLogin) {
            $authorId = $this->haveUser($authorLogin);
            $this->haveSubscription($authorId, $followerId);
        }

        return $followerId;
    }
    ```
* Добавляем новые тесты в класс `TestsSubscriptionCest`
    ```php
    public function testListAuthorsUnauthorized(AcceptanceTester $I): void
    {
        $I->sendGet('/api/v1/subscription/list-by-follower?authorId=1');
        $I->canSeeResponseCodeIs(HttpCode::UNAUTHORIZED);
    }

    public function tesListAuthorsAsAdmin(AcceptanceTester $I): void
    {
        $followerId = $I->haveAuthorWithFollowers('John Smith', ['Some follower']);
        $I->amAdmin();
        $I->sendGet("/api/v1/subscription/list-by-follower?followerId=$followerId");
        $I->canSeeForbiddenResponse();
    }

    public function testListAuthorsAsUser(AcceptanceTester $I): void
    {
        $author1 = 'Leo Tolstoy';
        $author2 = 'Anton Chekhov';
        $author3 = 'Fyodor Dostoevsky';
        $follower1Id = $I->haveFollowerWithAuthors('Peter', [$author3, $author1]);
        $follower2Id = $I->haveFollowerWithAuthors('Kate', [$author2]);
        $I->amUser();
        $I->sendGet("/api/v1/subscription/list-by-follower?followerId=$follower1Id");
        $I->canSeeResponseCodeIs(200);
        $I->canSeeResponseContainsJson(['login' => $author1]);
        $I->canSeeResponseContainsJson(['login' => $author3]);
        $I->haveCountItemsInResponseByJsonPath('authors', 2);
        $I->sendGet("/api/v1/subscription/list-by-follower?followerId=$follower2Id");
        $I->canSeeResponseCodeIs(200);
        $I->canSeeResponseContainsJson(['login' => $author2]);
        $I->haveCountItemsInResponseByJsonPath('authors', 1);
    }
    ```
* Снова запускаем тесты командой `vendor/bin/codecept run tests/acceptance/SubscriptionCest.php`, проверяем, что всё работает
* Создаём файл `tests/acceptance/TweetCest.php`
    ```php
    <?php
    
    namespace Tests;
    
    use CodeceptionUtilHttpCode;
    
    class TweetCest
    {
        public function testPostTweetUnauthorized(AcceptanceTester $I): void
        {
            $I->sendPost('/api/v1/tweet', ['authorId' => 1, 'text' => 'Some text']);
            $I->canSeeResponseCodeIs(HttpCode::UNAUTHORIZED);
        }
    
        public function testPostTweetAsAdmin(AcceptanceTester $I): void
        {
            $authorId = $I->haveUser('Lewis Carroll');
            $I->amAdmin();
            $I->sendPost('/api/v1/tweet', ['authorId' => $authorId, 'text' => 'Tweet']);
            $I->canSeeForbiddenResponse();
        }
    
        public function testPostTweetAsUser(AcceptanceTester $I): void
        {
            $authorId = $I->haveUser('Lewis Carroll');
            $I->amUser();
            $I->sendPost('/api/v1/tweet', ['authorId' => $authorId, 'text' => 'Alice in Wonderland']);
            $I->canSeeResponseCodeIs(200);
            $I->canSeeResponseContainsJson(['success' => true]);
            $I->canSeeResponseMatchesJsonType(['success' => 'boolean']);
        }

        public function testPostTweetBadRequest(AcceptanceTester $I): void
        {
            $I->amUser();
            $I->sendPost('/api/v1/tweet');
            $I->canSeeBadRequestResponse();
        }
    }
    ```
* Запускаем тесты командой `vendor/bin/codecept run tests/acceptance/TweetCest.php`, проверяем, что всё работает
* Добавляем в класс `AppTestsAcceptanceTester` новый метод:
    ```php
    public function haveTweet(int $authorId, string $text): void
    {
        $this->amUser();
        $this->sendPost('/api/v1/tweet', ['authorId' => $authorId, 'text' => $text]);
    }
    ```
* Добавляем новые тесты в класс `TestsTweetCest`
    ```php
    public function testGetFeedUnauthorized(AcceptanceTester $I): void
    {
        $I->sendPost('/api/v1/feed?userId=1&count=3');
        $I->canSeeResponseCodeIs(HttpCode::UNAUTHORIZED);
    }
    
    public function testGetFeedAsAdmin(AcceptanceTester $I): void
    {
        $followerId = $I->haveUser('Victor');
        $I->amAdmin();
        $I->sendPost("/api/v1/feed?userId=$followerId&count=3");
        $I->canSeeForbiddenResponse();
    }
    
    public function testGetFeedAsUser(AcceptanceTester $I): void
    {
        $author1Id = $I->haveUser('Charles Dickens');
        $author2Id = $I->haveUser('Jerome K Jerome');
        $author3Id = $I->haveUser('Jerome Salinger');
        $followerId = $I->haveUser('Julie');
        $I->haveSubscription($author1Id, $followerId);
        $I->haveSubscription($author2Id, $followerId);
        $I->haveTweet($author1Id, 'Pickwick Papers');
        $I->haveTweet($author2Id, 'Three Men in a Boat');
        $I->haveTweet($author3Id, 'Catcher in the Rye');
        $I->haveTweet($author1Id, 'Oliver Twist');
        $I->haveTweet($author2Id, 'Three Men on Wheels');
        $I->sendGet("/api/v1/tweet/feed?userId=$followerId&count=2");
        $I->canSeeResponseCodeIs(200);
        $I->haveCountItemsInResponseByJsonPath('tweets', 2);
        $I->canSeeResponseContainsJson(['text' => 'Three Men on Wheels']);
        $I->canSeeResponseContainsJson(['text' => 'Oliver Twist']);
        $I->sendGet("/api/v1/tweet/feed?userId=$followerId&count=10");
        $I->canSeeResponseCodeIs(200);
        $I->haveCountItemsInResponseByJsonPath('tweets', 4);
        $I->canSeeResponseContainsJson(['text' => 'Three Men on Wheels']);
        $I->canSeeResponseContainsJson(['text' => 'Oliver Twist']);
        $I->canSeeResponseContainsJson(['text' => 'Pickwick Papers']);
        $I->canSeeResponseContainsJson(['text' => 'Three Men in a Boat']);
    }
    ```
* Снова запускаем тесты командой `vendor/bin/codecept run tests/acceptance/TweetCest.php`, проверяем, что всё работает
* Устанавливаем модуль `WebDriver` командой
    ```shell script
    composer require --dev codeception/module-webdriver
    ```
* Создаём файл `docker-compose.yml`
    ```yaml
    version: '3.1'
    
    services:
    
      selenium-chrome:
        image: selenium/standalone-chrome
        shm_size: '2gb'
        container_name: 'selenium-chrome'
        ports:
          - 4444:4444
    ```
* Запускаем контейнер `docker-compose up -d`
* В файле `composer.json` исправляем секцию `autoload-dev`
    ```json
    "autoload-dev": {
        "psr-4": {
            "Tests\": "tests/acceptance",
            "SeleniumTests\": "tests/selenium"
        }
    },
    ```
* Создаём файл `tests/selenium.suite.yml`
    ```yaml
    actor: SeleniumTester
    modules:
      enabled:
        - WebDriver:
            url: 'http://devenergy.ru'
            browser: chrome
    ```
* Генерируем акторы командой `vendor/bin/codecept build`
* Создаём файл `tests/selenium/DevEnergyCest.php`
    ```php
    <?php
    
    namespace SeleniumTests;
    
    use TestsSeleniumTester;
    
    class DevEnergyCest
    {
        public function testPageTitle(SeleniumTester $I): void
        {
            $I->amOnPage('/');
            $I->seeInTitle('/dev/energy');
        }
    }
    ```
* Запускаем тесты командой `vendor/bin/codecept run tests/selenium/DevEnergyCest.php`, проверяем, что всё работает
* Добавляем новый тест в класс `TestsDevEnergyCest`
    ```php
    public function testAboutPage(SeleniumTester $I): void
    {
        $I->amOnPage('/');
        $I->click('#menu-item-482');
        $I->canSee('О сайте и об мне');
    }
    ```
* Снова запускаем тесты командой `vendor/bin/codecept run tests/selenium/DevEnergyCest.php`, видим ошибку, т.к. текст
   на сайте не совпадает с текстом в тесте (об мне !== обо мне)
* Исправляем ошибку в тесте
    ```php
    public function testAboutPage(SeleniumTester $I): void
    {
        $I->amOnPage('/');
        $I->click('#menu-item-482');
        $I->canSee('О сайте и обо мне');
    }
    ```
* Снова запускаем тесты командой `vendor/bin/codecept run tests/selenium/DevEnergyCest.php`, видим, что всё работает
* Добавим ещё один тест
    ```php
    public function testFeedbackPage(SeleniumTester $I): void
    {
        $I->amOnPage('/');
        $I->click('#menu-item-491');
        $I->fillField(['name' => 'your-name'],'Me');
        $I->fillField(['name' => 'your-email'],'my@mail.ru');
        $I->click('input[type=submit]');
        $I->canSee('Спасибо за ваше сообщение');
    }
    ```
* Снова запускаем тесты командой `vendor/bin/codecept run tests/selenium/DevEnergyCest.php`, видим ошибку из-за того,
   что сообщение не успело отобразиться
* Добавим ожидание
    ```php
    public function testFeedbackPage(SeleniumTester $I): void
    {
        $I->amOnPage('/');
        $I->click('#menu-item-491');
        $I->fillField(['name' => 'your-name'],'Me');
        $I->fillField(['name' => 'your-email'],'my@mail.ru');
        $I->click('input[type=submit]');
        $I->wait(1);
        $I->canSee('Спасибо за ваше сообщение');
    }
    ```
* Снова запускаем тесты командой `vendor/bin/codecept run tests/selenium/DevEnergyCest.php`, видим, что всё работает

Мой отзыв о занятии

  • Было трудно: понять, как все работает, без возможности самостоятельного запуска тестов (параллельно с лектором)

  • Меня удивило: что я не использовал Codeception/Selenium до сих пор в своих проектах (хотя опыт работы с ним уже был, ранее)

  • Я понял: что пора писать тесты на Codeception (помимо phpunit)


Дополнительные материалы


Tags

Нет комментариев

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.