Testify: выбор пакета для тестирования (require vs assert) | OTUS
🔥 Начинаем BLACK FRIDAY!
Максимальная скидка -25% на всё. Успейте начать обучение по самой выгодной цене.
Выбрать курс

Курсы

Программирование
iOS Developer. Basic
-25%
Python Developer. Professional
-25%
Разработчик на Spring Framework
-25%
Golang Developer. Professional
-25%
Python Developer. Basic
-25%
iOS Developer. Professional
-25%
Node.js Developer
-25%
Unity Game Developer. Professional
-25%
React.js Developer
-25%
Android Developer. Professional
-25%
Software Architect
-25%
C++ Developer. Professional
-25%
Программист С Разработчик C# Базы данных MS SQL Server Developer AWS для разработчиков Cloud Solution Architecture Разработчик голосовых ассистентов и чат-ботов Архитектура и шаблоны проектирования Agile Project Manager Нереляционные базы данных Супер - интенсив по паттернам проектирования Супер-практикум по использованию и настройке GIT IoT-разработчик Подготовка к сертификации Oracle Java Programmer (OCAJP) Супер-интенсив «СУБД в высоконагруженных системах» Супер-интенсив "Azure для разработчиков"
Инфраструктура
Мониторинг и логирование: Zabbix, Prometheus, ELK
-25%
DevOps практики и инструменты
-25%
Архитектор сетей
-25%
Инфраструктурная платформа на основе Kubernetes
-25%
Супер-интенсив «ELK»
-16%
Супер-интенсив «IaC Ansible»
-16%
Administrator Linux. Professional MS SQL Server Developer Безопасность Linux PostgreSQL Reverse-Engineering. Professional CI/CD VOIP инженер Супер-практикум по работе с протоколом BGP Супер - интенсив по паттернам проектирования Супер - интенсив по Kubernetes Administrator Linux.Basic Супер-интенсив "Tarantool"
Специализации Курсы в разработке Подготовительные курсы
+7 499 938-92-02

Testify: выбор пакета для тестирования (require vs assert)

Go_Deep_24.4-5020-47fabf.png

Testify — фреймворк для написания тестов на go. Если открыть документацию, то сразу видно, что в нём два основных пакета asser и require. И в обоих одинаковый набор методов.

Какой пакет использовать?

Я советую всем использовать require. Всегда, когда не знаете, нет времени или желания думать — используйте require.

Обратите внимание на «нет времени или желания думать». При написании тестов и так хватает, о чём подумать. Поэтому лучше ставить require и не думать об этом.

В чём разница между двумя пакетами?

Require сразу останавливает выполнение теста. Assert пометит тест упавшим, но выполнение продолжится.

Что случится, если сделать наоборот и всегда ставить assert?

resp, err := client.Do(req)
assert.NoError(t, err)
assert.Equal(t, len(expectedBody), resp.ContentLength)

Это код упадёт в панику, если client.Do вернёт ошибку (resp будет nil, и при попытке обратиться к ContentLength произойдёт разыменовывание нулевого указателя):

--- FAIL: TestFoo (0.00s)
    /Users/kulti/projects/blog_testify/assertions.go:239: 

    Error Trace:    foo_test.go:27

    Error:          Expected nil, but got: &errors.errorString{s:"test"}
panic: runtime error: invalid memory address or nil pointer dereference [recovered]
    panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x1240743]

goroutine 6 [running]:
testing.tRunner.func1(0xc42010c0f0)
    /usr/local/Cellar/go/1.10.1/libexec/src/testing/testing.go:742 +0x29d
panic(0x1284320, 0x1450180)
    /usr/local/Cellar/go/1.10.1/libexec/src/runtime/panic.go:502 +0x229
_/Users/kulti/projects/blog_testify.TestFoo(0xc42010c0f0)
    /Users/kulti/projects/blog_testify/foo_test.go:28 +0xc3
testing.tRunner(0xc42010c0f0, 0x12ea138)
    /usr/local/Cellar/go/1.10.1/libexec/src/testing/testing.go:777 +0xd0
created by testing.(*T).Run
    /usr/local/Cellar/go/1.10.1/libexec/src/testing/testing.go:824 +0x2e0
exit status 2
FAIL    _/Users/kulti/projects/blog_testify 0.021s
Error: Tests failed.

Assert как бы говорит: «Я тут что-то проверил, но ронять тест не буду». Поэтому код теста должен быть готов, что что-то уже пошло не так. Это усложняет понимание теста. Чтобы не думать, что там случится после assert’a, я рекомендую всегда использовать require:

resp, err := client.Do(req)
require.Nil(t, err)
require.Equal(t, len(expectedBody), resp.ContentLength)

--- FAIL: TestFoo (0.00s)
    /Users/kulti/projects/blog_testify/assertions.go:239: 

    Error Trace:    foo_test.go:26

    Error:          Expected nil, but got: &errors.errorString{s:"test"}
FAIL
exit status 1
FAIL    _/Users/kulti/projects/blog_testify 0.030s
Error: Tests failed.

Зачем же этот assert вообще нужен?

Рассмотрим пример. Есть табличный тест:

cases := []testCase{
    {"", 0},
    {"v1", len("v1")},
    {"V1", len("v12")},
}


for _, c := range cases {
    resp, err := fn(c.str)
    require.NoError(t, err) {
    require.Equal(t, c.expected, resp.ContentLength)
}

Здесь require работает не очень хорошо, т. к. завалит тест сразу, как только провалится один из случаев. Хотелось бы проверить все случаи из cases.

Assert не просто помечает, что надо в конце завалить тест, он возвращает bool, говорящий, прошла проверка или нет. Если это использовать, то можно пройтись по всем тест-кейcам из таблицы так:

for _, c := range cases {
    resp, err := fn(c.str)
    if assert.NoError(t, err) {
        assert.Equal(t, c.expected, resp.ContentLength)
    }
}

Небольшой минус такого решения — это сообщения об ошибках:

--- FAIL: TestFoo (0.00s)
    /Users/kulti/projects/blog_testify/assertions.go:239: 

    Error Trace:    foo_test.go:37

    Error:          Expected nil, but got: &errors.errorString{s:"test"}
    /Users/kulti/projects/blog_testify/assertions.go:239: 

    Error Trace:    foo_test.go:38

    Error:          Not equal: 

                    expected: 2

                    actual: 0
    /Users/kulti/projects/blog_testify/assertions.go:239: 

    Error Trace:    foo_test.go:38

    Error:          Not equal: 

                    expected: 3

                    actual: 0
FAIL
exit status 1
FAIL    _/Users/kulti/projects/blog_testify 0.018s
Error: Tests failed.

Какой именно случай упал — не очень понятно. Можно доработать assert’ы, добавив к ним расширенное сообщение об ошибке:

assert.NoErrorf(t, err, “check: %v”, c)
assert.Equalf(t, c.expected, resp.ContentLength, “check: %v”, c)

Правда придётся так проработать все assert’ы. В примере их два, а что, если быдл бы больше? Я рекомендую использовать subtest:

for _, c := range cases {
    t.Run("check "+c.str, func(t *testing.T) {
        resp, err := fn(c.str)
        require.Nil(t, err)
        require.Equal(t, c.expected, resp.ContentLength)
    })
}

Во-первых, тут описание случая находится сразу в названии теста:

--- FAIL: TestFoo (0.00s)
    --- FAIL: TestFoo/check_ (0.00s)
        /Users/kulti/projects/blog_testify/assertions.go:239: 

    Error Trace:    foo_test.go:38

    Error:          Expected nil, but got: &errors.errorString{s:"test"}
    --- FAIL: TestFoo/check_v1 (0.00s)
        /Users/kulti/projects/blog_testify/assertions.go:239: 

    Error Trace:    foo_test.go:39

    Error:          Not equal: 

                    expected: 2

                    actual: 0
    --- FAIL: TestFoo/check_V1 (0.00s)
        /Users/kulti/projects/blog_testify/assertions.go:239: 

    Error Trace:    foo_test.go:39

    Error:          Not equal: 

                    expected: 3

                    actual: 0

А во-вторых, можно спокойно использовать require, т.к. это приведёт к завершению одно сабтеста. Раз нет assert'ов — можно не волноваться, что что-то упадёт из-за непродуманных зависимостей, код проще читать и понимать, т.к. он развивается линейно, без ветвлений и скрытых состояний.

Резюме

  1. Используйте require.
  2. Если require мешается, попробуйте реорганизовать тест: использовать сабтесты, разбить на несколько тестов.
  3. Assert усложняет читаемость теста — нужны очень веские причины, чтобы его использовать.

Не пропустите новые полезные статьи!

Спасибо за подписку!

Мы отправили вам письмо для подтверждения вашего email.
С уважением, OTUS!

Автор
0 комментариев
Для комментирования необходимо авторизоваться
🎁 Максимальная скидка!
Черная пятница уже в OTUS! Скидка -25% на всё!