Юнит тесты PHPUnit

oxy

Знаток
🏆
📜
Сообщения
502
Реакции
152
В этой статье я хочу обратить внимание на необходимость автоматизированного тестирования ваших приложений...

Конечно я уверен что для большей части посетителей форума эта тема не то что не интересна а попросту не нужна.
В общем если у тебя до сих пор php 5.4 и mysql (не mysqli или PDO), тесты тебе точно не нужны... Все остальные могут читать дальше))

Что же такое автоматизированное тестирование? Тут все просто, это набор методов которые автоматом тестируют твой код.
Ведь мы все сталкивались с проблемой когда меняешь маленькую часть кода, а через день тебе приходят посетители твоего ресурса и говорят что перестала работать например регистрация... Так вот что бы держать все в актуальном и проверенном состоянии и нужны тесты.

Допустим у тебя есть метод который возвращает форму отправки письма. Для проверки нам достаточно написать код ниже:


PHP:
class MailFormTest extends TestCase
{
    public function testMailForm()

    {
        $client = new GuzzleHttp\Client(); // Просто клиент делающий запросы, хочешь юзай Curl
        $response = $client->get('http://your_route');

        $this->assertSame(200, $response->getStatusCode()); // Сами тесты
        $this->assertStringContainsString('form id="mail-form"', $response->getBody());
    }
}

И все.. Ничего сложного так ведь?

Теперь разберемся что тут вообще происходит:
  1. Первым делом мы делаем запрос к своему проекту
  2. Дальше сравниваем статус ответа (Ведь если ты сломал где то подключение к БД то при правильной настройке у тебя система должна отдать что угодно, но не 200й ответ).
  3. Но даже если ты дятел и не настроил все как нужно, вторым сравнением мы ищем на странице форму с ID mail-form и точно уверенны что она показывается пользователю...

Теперь минусы сложности:
  • Тестов у тебя будет очень много... Порой на один метод тестируемого класса нужно писать 5-10 тестов (Может ли авторизированный юзер сделать это, может незалогиненный юзер получить результат, что если передать не правильные параметры в форму и т.д.)
  • За счет написания тестов увеличивается код и время на его разработку. Ведь раньше ты писал 1 метод и радовался жизни, а теперь нужно написать его и пачку разных тестов к нему (впрочем тут поможет наследование и некая шаблонизация, в итоге ты просто передашь массив из нужных урлов, статусов ответа и контента который должен отобразиться на странице)
  • Изменив что то на странице (например форма теперь называется как то по другому), тебе нужно править и тесты под это дело
  • Нужно подключать composer и его autoloader

Но все это с лихвой компенсируется уверенностью что у тебя все всегда работает так как тебе нужно!

  • Ты можешь тестировать классы и сервисы а не только роуты (Но тут немного сложнее и возможно потребуется моккинг)
  • Совсем не обязательно покрывать тестами весь свой код, можно покрыть самую важную часть
  • Ты всегда знаешь что у тебя отвалилось и при добавленнии новой фичи ты всегда уверен что она работает так как ты хотел и никак иначе
  • Безопасность. Да да, тебе ничего не мешает написать тест на SQL инекцию или какой другой хак
  • Ты даже можешь замерять время выполнения и сказать что оно должно быть меньше N секунд. Если это не так, ты сразу узнаешь что в коде что то не так...
  • Можно тестировать фронт, есть куча либ типа Selenium позволяющих убедиться что каждый элемент на странице на своем месте и ничего не съехало
  • Можно использовать фейкеры, когда у тебя автоматом создается модель с уже заполненными, нужными тебе параметрами

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

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

Insallah

Эксперт
👑
🏆
📜
Сообщения
1,689
Реакции
541
Сколько я ни пытался в эту тему заехать.. Видимо пока по работе не столкнусь, не пойму что к чему.
 

Антон

Интересующийся
📜
Сообщения
129
Реакции
12
И все.. Ничего сложного так ведь?
Как раз читаю юнитесты(но с#), действительно, ничего сложного)
Код:
public class NavigationMenuViewComponentTest
    {
        [Fact]
        public void Can_Select_Catigories()
        {
            Mock<IProductRepository> mock = new Mock<IProductRepository>();
            mock.Setup(m => m.Products).Returns((new Product[] {
                new Product {ProductId = 1, Name = "P1", Category = "Apples" },
                new Product {ProductId = 2, Name = "P2", Category = "Apples" },
                new Product {ProductId = 3, Name = "P3", Category = "Plums" },
                new Product {ProductId = 4, Name = "P4", Category = "Oranges" }
                }).AsQueryable<Product>());

            NavigationMenuViewComponent target = new NavigationMenuViewComponent(mock.Object);

            string[] result = ((IEnumerable<string>)(target.Invoke() as ViewViewComponentResult).ViewData.Model).ToArray();

            Assert.True(Enumerable.SequenceEqual(new string[] { "Apples, Oranges, Plums" }, result));
        }

        [Fact]
        public void Indicates_Selected_Category()
        {
            string CategoryToSelect = "Apples";
            Mock<IProductRepository> mock = new Mock<IProductRepository>();
            mock.Setup(m => m.Products).Returns((new Product[]
            {
                new Product {ProductId = 1, Name = "P1", Category = "Apples"},
                new Product {ProductId = 2, Name = "P2", Category = "Oranges"}
            }).AsQueryable<Product>());

            NavigationMenuViewComponent target = new NavigationMenuViewComponent(mock.Object);
            target.ViewComponentContext = new ViewComponentContext { ViewContext = new ViewContext { RouteData = new RouteData() } };

            target.RouteData.Values["category"] = CategoryToSelect;
            string result = (string)(target.Invoke() as ViewViewComponentResult).ViewData["SelectedCategory"];

            Assert.Equal(CategoryToSelect, result);
        }
    }
Страшно представить что творится в реальных проектах)
 
Последнее редактирование:

oxy

Знаток
🏆
📜
Сообщения
502
Реакции
152
Почему страшно? Ты по большому счету все руками делаешь, а есть еще data providers и прочее хелперы.
В итоге при правильном подходе тесты выглядят не так и страшно на гите полно примеров
 

oxy

Знаток
🏆
📜
Сообщения
502
Реакции
152
Вот к примеру часть кода отвечающая за проверку сгенерированного объекта, что все данные разлетелись по нужным ключам, с нужными типами данных и т.д.

PHP:
class RuleTest extends TestCase
{

    /**
     * @test
     *
     * @throws \JsonException
     */
    public function createRuleFromArray()
    {
        $init = [
            'id'      => 123, 
            'query' => 'test query',
            'tags'    => json_encode([
                'tag'           => 'myCustomTag',
                'inserted'      => '2021-11-13 11:47:43',
                'updated'       => '2021-11-13 11:47:43',
                'originalQuery' => 'myOriginalQuery',
                'externalQuery' => 'myExternalQuery',
                "highlighting"  => true,
                "variables"     => "",
            ], JSON_THROW_ON_ERROR),
            'filters' => 'json.tag = "custom"',
        ];

        $rule = new \App\Models\Rule();
        $rule->init($init);

        self::assertSame($rule->getQuery(), $init['query']);
        self::assertSame($rule->getFilters(), $init['filters']);
        self::assertSame($rule->getId(), $init['id']);

        $tags = json_decode($init['tags']);
        self::assertSame($rule->getTags()->getTag(), $tags->tag);
        self::assertSame($rule->getTags()->getInserted(), $tags->inserted);
        self::assertSame($rule->getTags()->getUpdated(), $tags->updated);
        self::assertSame($rule->getTags()->getOriginalQuery(), $tags->originalQuery);
        self::assertSame($rule->getTags()->getExternalQuery(), $tags->externalQuery);
        self::assertSame($rule->getTags()->getHighlighting(), $tags->highlighting);
        self::assertSame($rule->getTags()->getVariables()->all(), (new Collection())->all());
    }
}

Да новичку может показаться что данный код избыточный и зачем это вообще делать. Но в моем случае внутри модели куча всяких методов, и благодаря этим 20 строкам я всегда уверен что код ВСЕГДА будет работать так как я изначально задумал, и если я полезу внутрь что то менять, я сразу увижу что я сломал!
 

Insallah

Эксперт
👑
🏆
📜
Сообщения
1,689
Реакции
541
Окей, ты создал набор тестовых переменных и потом.. что? Гнать их по боевому проекту - что-то не то. Гнать по спец-тестам строго под эти переменные, надо следить, чтобы тест всегда в точности соответствовал методу, который он охватывает и в случае модификации метода модифицировать и тест. Или соль как раз в этом?
 

oxy

Знаток
🏆
📜
Сообщения
502
Реакции
152
Обычно тесты запускаются автоматом через GitlabCi и гоняются на каждый коммит. По тому конечно на боевом сервере никаких прогонов нет.

На счет модификации именно так как ты говоришь. Если ты что то модифицируешь в классе то тесты ты тоже дорабатываешь под это. Да это лишняя возня, но это и есть плата за стабильность.
Особенно актуально к случае через год ты решил что то доработать свой класс и 100% ты не помнишь как оно там под капотом работает. Меняешь пару строк и понимаешь что эта замена у тебя задела такие то тесты и они начали фейлиться.

Задаешься вопросом должно ли было тут измениться поведение или нет. Нет не должно, значит накосячил....
 

oxy

Знаток
🏆
📜
Сообщения
502
Реакции
152
Забыл указать специфику моего теста, там нельзя добавить кастомное поле, по тому все приходится пихать в поле тег.

Что бы не вводило это вас в заблуждение почему я какие то параметры (типа updated, highlighting etc) храню в тегах
 

Insallah

Эксперт
👑
🏆
📜
Сообщения
1,689
Реакции
541
Хорошо, что я этим деньги не зарабатываю, а то я бы повесился. :)
 

k880TR

Интересующийся
📜
Сообщения
95
Реакции
14
Помню как на ангуляре делал тесты, как начал вникать муть конкретная и не понятная. Но когда написал пару тестов уже становится понятно для чего оно вообще))))
 
Внимание! Эта тема устарела на 187 дней.
Тут обсуждать нечего, лучше создай новую тему. Конечно, если очень-очень нужно (например хочешь ответить на древний вопрос), то отвечай. Но помни: некропост — зло, а модератор не дремлет!
Сверху