---
title: "Первый тест: assert.Equal и go test"
description: "Первый тест на Go с assert.Equal: запускаем go test -v, читаем diff-отчёт о провале и ловим ошибку с перепутанными expected и actual. Пошаговый разбор."
course: "Testify: тестирование Go с assert и require"
url: "https://golangmaster.ru/courses/testify/lessons/02-pervyj-assert/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 18
---

# Первый тест: assert.Equal и go test

*Курс «Testify: тестирование Go с assert и require», урок 2 из 25. ~18 минут. Обновлено 21.09.2026. Источник: https://golangmaster.ru/courses/testify/lessons/02-pervyj-assert/*

Первый тест go с assert.Equal заканчивается не зелёным ok, а умением читать провал: testify печатает expected и actual с diff, и по этому отчёту баг находится за секунды. Соберём тест с нуля: сигнатура TestXxx, запуск go test -v, нарочито сломанная проверка и разбор отчёта построчно.

Библиотека [установлена в прошлом уроке](/courses/testify/lessons/01-znakomstvo-testify/) и пока не сделала ничего: сам по себе импорт в тест-файле не запускает ни одной проверки. Соберём теперь настоящий тест целиком — от пустого файла до отчёта о провале — и пойдём не по гладкой дорожке: второй прогон будет красным, и в этом главный план урока. Провал testify-теста — не просто строка FAIL: отчёт показывает ожидаемое значение, фактическое, diff между ними и адрес строки, где проверка не сошлась. Кто умеет читать этот отчёт, тот чинит регрессии заметно быстрее того, кто перечитывает логи. По пути разберём анатомию тестовой функции, флаг -v, статусы go test и самую частую ошибку новичка — перепутанные местами expected и actual. Полный прогон, напомню, требует локального Go: браузерная песочница не исполняет пакеты тестирования, поэтому запускаем всё в терминале своей машины.

### Анатомия первого теста: TestXxx(t *testing.T)

Тест в Go — обычная функция с тремя условиями. Первое: файл с суффиксом _test.go, иначе go test его не соберёт — суффикс, не префикс, и это частая опечатка у пришедших из других языков. Второе: имя начинается с Test и продолжается заглавной буквой — TestParseUser, TestCalcSum; компилятор равнодушен к testparseuser, но runner такой функции не найдёт. Третье: единственный аргумент t *testing.T — через него testify и весь reporting общаются с пакетом testing: все assert-функции принимают t первым аргументом и отмечают провалы именно на нём. Забытый t не скомпилируется, лишний параметр — тоже. Пакет у теста тот же, что у тестируемого кода: Split лежит в calc.go пакета calc, тест — в calc_test.go того же пакета, и непубличные идентификаторы видны напрямую. Альтернативный пакет calc_test с внешним API пригодится позже, в теме заглушек. Сигнатура фиксирована: func TestXxx(t *testing.T), ничего не возвращает — на этом магия заканчивается, дальше обычный код.

### Тест, который должен пройти

```go
// calc_test.go
package calc

import (
        "testing"

        "github.com/stretchr/testify/assert"
)

// Split делит строку по разделителю и возвращает число частей
func TestSplit(t *testing.T) {
        got := Split("a,b,c", ",")

        assert.Equal(t, 3, got)
}
```

Файл в пакете calc, функция с Test и t *testing.T, внутри один assert.Equal: ожидаемое 3 вторым аргументом, фактическое got третьим. Комментарий над Split — не украшение: тест должен читаться отдельно от кода. В реальном проекте Split живёт в calc.go, а тест — в calc_test.go рядом; локальный прогон go test ./... покажет зелёное ok calc.

### Запуск: go test и флаг -v

Команда go test запускается в каталоге пакета, а go test ./... — по всем пакетам модуля разом. По умолчанию вывод скромен до аскетизма: ok calc или FAIL calc — и всё, имена прошедших тестов не печатаются. Флаг -v (verbose) разворачивает историю: каждый тест получает строку === RUN с именем и завершение PASS или FAIL с временем, и по журналу видно, какие проверки вообще выполнялись. Второй полезный флаг — -run: go test -run TestSplit гоняет только совпавшие по имени тесты, и на большом пакете экономит минуты. Третья деталь, которая сбивает с толку: go test кэширует результаты — неизменившийся пакет отвечает ok с пометкой (cached) и по-настоящему не перезапускается; свежий прогон задаётся флагом -count=1. Как и весь курс, полный прогон требует локального Go — браузерная песочница не исполняет пакеты тестирования, поэтому здесь терминал заменяют интерактивы: квизы, сборка отчёта по строкам, чтение вывода по частям.

### Нарочный провал и его отчёт

```go
func TestSplit(t *testing.T) {
        got := Split("a,b", ",")

        assert.Equal(t, 3, got)
}

// go test -v
// === RUN   TestSplit
//     calc_test.go:11:
//         Error Trace:	calc_test.go:11
//         Error:       Not equal:
//                      expected: 3
//                      actual  : 2
//                      Test:        TestSplit
// --- FAIL: TestSplit (0.00s)
// FAIL
// FAIL    calc    0.002s
```

Разница с рабочим тестом — одна буква: Split получает a,b вместо a,b,c и возвращает 2. Тест падает, и testify печатает отчёт: адрес строки, Not equal, ожидаемое 3, фактическое 2, имя теста. Падение локализовано: --- FAIL относится к TestSplit, а не ко всему пакету — остальные тесты файла выполняются как ни в чём не бывало.

### Читаем отчёт о провале

Отчёт читается сверху вниз по четырём слоям. Слой запуска: `=== RUN TestSplit` — тест стартовал. Слой деталей: строка с адресом файла и строки проверки, затем `Error: Not equal` и два значения — expected, то, что вы передали вторым аргументом, и actual, то, что пришло из кода; testify печатает ещё строку Test с именем — удобно, когда журнал длинный. Слой вердикта: `--- FAIL: TestSplit (0.00s)` с временем. Слой итога: FAIL пакета и суммарное время; команда возвращает код выхода 1 — CI краснеет именно из-за него, а не из-за текста. Заметьте тонкость: testify пишет слова «ожидаемое» и «фактическое», но не знает, кто из них прав в вашей предметной области, — библиотека лишь честно показывает, что вы передали. Кто есть кто, решаете вы, и здесь критичен порядок аргументов: о классической ошибке с перепутанными значениями — ниже.

> **Перепутанные expected и actual.** Классическая ошибка первого теста: assert.Equal(t, got, want) — значения поменяны местами. Тест упадёт так же, отчёт останется читаемым, но diff перевёрнут: «ожидаемым» станет то, что реально пришло из кода, и баг при поиске уводит в обратном направлении. Дисциплина простая: вторым аргументом — константа или эталон, третьим — результат вызова. Читая чужие провалы, сверяйте diff с кодом строки из Error Trace.

Что происходит при нескольких провалах в одном тесте. Проверки assert не прерывают функцию: первый упавший Equal лишь вносит строку в отчёт, и выполнение идёт дальше. Следствие — за один прогон видны все расхождения сразу: если из пяти проверок не сошлись три, отчёт покажет три блока expected/actual подряд, а не первый. Это экономит цикл «исправил — перезапустил — нашёл следующее», который на пакете из сотен тестов ощутимо дорог. Оборотная сторона: вторая проверка выполняется на данных, скомпрометированных первым провалом. Если результат парсинга оказался nil, следующие проверки не добавят информацию, а упадут с паникой и завалят отчёт стеком горутины — поэтому подготовку данных ведут через require, и это тема следующего урока. Паника в тесте тоже даёт FAIL, но выглядит иначе: вертикальный стек вызовов вместо аккуратного expected/actual. Увидеть такой отчёт хотя бы раз стоит — чтобы потом отличать мгновенно.

### Два провала в одном тесте

```go
func TestUser(t *testing.T) {
        user := ParseUser("ann", 17)

        assert.Equal(t, "ann", user.Name)
        assert.Equal(t, 18, user.Age)
        assert.Equal(t, "ann@example.com", user.Email)
}
```

Возраст 17 против ожидаемого 18 и пустой Email против ann@example.com — два провала; имя сойдётся. Отчёт покажет два блока Error по очереди, с адресами строк. Если ParseUser на плохом входе возвращает nil, обращение к полю упадёт с паникой — тот случай, когда нужна ранняя остановка require, и мы её разберём в третьем уроке.

### Мини-ритуал первого теста

- Файл с суффиксом _test.go в пакете тестируемого кода
- Функция TestXxx с единственным аргументом t *testing.T
- assert.Equal(t, эталон, результат) — ожидаемое вторым, фактическое третьим
- Подготовка данных — через require, чтобы тест останавливался на битом входе
- Запуск go test -v в каталоге пакета, go test ./... — по всему модулю
- Красный тест — не авария, а данные: читайте expected, actual и Error Trace

### Статусы и строки go test

| Что видите | Что это значит | Что делать |
| --- | --- | --- |
| ok calc | Все тесты пакета прошли | Ничего; с -v покажет имена и PASS по каждому |
| --- FAIL: TestSplit | Упал конкретный тест | Читать блок Error: expected, actual и адрес строки |
| FAIL calc | В пакете есть провалы или паника | Искать первый --- FAIL выше по выводу |
| [no test files] | go test не нашёл файлов _test.go | Проверить суффикс файла и каталог |
| ok calc 0.002s (cached) | Кэш: код не менялся, прогон честный, но старый | Свежий запуск — go test -count=1 |

### Проверка: порядок аргументов

**Вопрос:** Как правильно вызвать проверку равенства в testify?

- ✅ assert.Equal(t, want, got) — t первым, ожидаемое вторым, фактическое третьим
- assert.Equal(want, got, t) — значения первыми, t последним
- assert.Equal(got, want, t) — фактическое вторым, ожидаемое третьим
- assert.Equal(t, got, want) — фактическое вторым, ожидаемое третьим

Сигнатура assert.Equal(t, expected, actual): *testing.T всегда первым, эталон вторым, результат кода третьим. Переставленные значения компилируются и работают, но переворачивают diff отчёта — самая частая ошибка первого теста, и из отчёта она видна плохо.

### Проверка: флаг -v

**Вопрос:** Что добавляет go test -v по сравнению с простым go test?

- Запускает тесты параллельно, поэтому быстрее
- Показывает покрытие строк процентами
- ✅ Печатает имена тестов: === RUN и PASS/FAIL по каждому
- Перезапускает упавшие тесты ещё два раза

Verbose-режим раскрывает журнал: каждый тест получает строки === RUN с именем и итог PASS или FAIL с временем. Параллельность задаёт t.Parallel, покрытие — флаг -cover, повторы — -count; -v отвечает только за подробность вывода.

### Что напечатает прогон?

```go
// Abs возвращает модуль числа
func TestAbs(t *testing.T) {
        got := Abs(-7)

        assert.Equal(t, 7, got)
}
```

**Что выведет программа?**

- ✅ ok: тест пройдёт — модуль минус семи равен семи
- FAIL: expected 7, actual -7
- FAIL: expected -7, actual 7
- Компиляция сломается: Abs не определён

Abs(-7) равен 7, проверка сходится, и прогон напечатает === RUN TestAbs, затем PASS TestAbs и ok пакета. Мораль: читая чужой тест, мысленно исполняйте вызов — эталон 7 стоит вторым аргументом, фактический результат третьим, и расхождений здесь нет.

### Допишите проверку

Восстановите тип t, имя пакета проверки и поле структуры.

```go
func TestParseUser(t *testing.___) {
        got := ParseUser("ann", 18)

        assert.___(t, "ann", got.Name)
        assert.Equal(t, 18, got.___)
}
```

Подсказка: Аргумент тестовой функции имеет тип T; равенство проверяет Equal; возраст живёт в поле Age.

Правильные ответы в пропусках: `T`, `Equal`, `Age`

### Соберите отчёт о провале

Расставьте строки вывода go test -v по порядку — от запуска теста до итога пакета (вывод сокращён).

Правильный порядок строк:

1. `=== RUN   TestSplit`
2. `    Error Trace:	calc_test.go:11`
3. `    Error:       Not equal: expected: 3, actual: 2`
4. `--- FAIL: TestSplit (0.00s)`
5. `FAIL`
6. `FAIL    calc    0.002s`

### Карточки: первый тест

- **assert.Equal(t, want, got)** — Проверка равенства: *testing.T первым аргументом, эталон вторым, результат кода третьим. Провал печатает expected, actual и адрес строки.
- **TestXxx(t *testing.T)** — Сигнатура тестовой функции: имя с Test и заглавной буквы, один аргумент t, без возвращаемого значения. Без t runner тест не увидит.
- **Файл _test.go** — Тесты живут в файлах с суффиксом _test.go в пакете тестируемого кода; go test остальные файлы не рассматривает.
- **go test -v** — Verbose-режим: === RUN и PASS/FAIL по каждому тесту. -run сужает прогон до совпавших имён, -count=1 обходит кэш результатов.
- **--- FAIL: TestSplit** — Вердикт упавшего теста: выше — блок Error Trace и Error с expected и actual, ниже — FAIL пакета и код выхода 1.
- **expected и actual** — expected — второй аргумент вызова, actual — третий. Перепутанные местами значения не ломают тест, но переворачивают diff и уводят поиск бага в обратную сторону.

> **Красный тест — это данные.** Не спешите чинить провал до чтения отчёта: expected и actual с адресом строки часто отвечают на вопрос целиком. Полезная привычка при отладке чужого бага — нарочно написать assert с гипотезой: если ожидание не сойдётся, отчёт покажет фактическое значение из глубины кода, и принт-отладка не понадобится.

### Спорное: один assert на тест

В TDD-мире живёт правило «один assert на тест»: тест должен падать по одной причине, и имя честно описывает провал. На практике testify оно ломается о первый же тест объекта: пять полей — пять вызовов Equal, и пять тестов с одинаковым парсингом ради каждого поля выглядят натужно. Моя позиция: несколько assert-ов в одном тесте стоит избегать не механически, а по границе — пока проверки относятся к одному сценарию одного объекта, они имеют право жить рядом; как только второй assert проверяет другой сценарий, тест делится. Компромисс, который работает на ревью: пачка Equal по полям одного результата — нормально, а assert на «и парсинг, и запись в базу, и ответ» — нет. Строгая школа права в одном: провалы внутри одного теста связаны, и require-версии с ранней остановкой дисциплинируют тут лучше любых правил.

### Что дальше

Первый тест собран, сломан и прочитан — дальше встаёт вопрос о судьбе провала: когда тест должен падать сразу, а когда полезно увидеть все расхождения. Это граница assert и require, и она размечена в [следующем уроке курса](/courses/testify/lessons/03-assert-require/). После require логичный шаг — проверки ошибок: Error, NoError и EqualError вместо if err != nil, тема [четвёртого урока](/courses/testify/lessons/04-error-assertions/). Примеры урока прогоняются локальным Go: соберите тест из блока выше, сломайте значение нарочно и прочитайте свой первый diff — рука запоминает этот отчёт быстрее, чем глаза. Оглавление и маршрут курса, если хотите опережать расписание, — на странице [курса testify](/courses/testify/).

## Коротко: что запомнить

- Тест — функция TestXxx(t *testing.T) в файле с суффиксом _test.go: без заглавной буквы после Test и без t runner тест не увидит.
- assert.Equal(t, want, got): t первым, эталон вторым, результат кода третьим — при провале testify печатает expected, actual и адрес строки.
- go test -v показывает === RUN и PASS/FAIL по каждому тесту; -run сужает прогон, -count=1 обходит кэш результатов.
- Отчёт провала читается слоями: RUN, блок Error с адресом строки и значениями, --- FAIL с именем, FAIL пакета и код выхода 1.
- Перепутанные expected и actual не ломают тест, но переворачивают diff: эталон — вторым аргументом, результат — третьим.
- Несколько assert в одном тесте падают вместе: за прогон видны все расхождения; битая подготовка данных — сигнал для require.
- Красный тест — данные: assert с гипотезой и его отчёт часто заменяют принт-отладку.

## Домашнее задание

Напишите тест к любой своей функции с assert.Equal и запустите go test -v — убедитесь в PASS. Затем нарочно поменяйте местами эталон и результат, перезапустите и сравните два отчёта: запомните, как выглядит перевёрнутый diff, чтобы узнавать его в чужих провалах.

## Вопросы и ответы по уроку

### Почему тест с ошибкой в имени не запускается, а компиляция проходит?

go test ищет функции по правилу: имя начинается с Test и продолжается заглавной буквой, аргумент t *testing.T. Всё остальное — обычные функции пакета. Компилятору такие функции корректны, поэтому ошибка видна только в выводе: «no tests to run» и отсутствие строки === RUN.

### Что делать, если go test показывает ok (cached), а я менял код?

Кэш сбрасывается изменением файла или переменных окружения; если результат всё равно старый, запустите go test -count=1 — он игнорирует кэш и выполняет тесты заново. Флаг -v к кэшу отношения не имеет: он управляет подробностью вывода, а не свежестью прогона.

### Можно ли запускать эти тесты в браузерной песочнице сайта?

Полный прогон go test — нет: песочница исполняет чистый Go без пакетов тестирования и модулей. Интерактивы урока — квизы, predict, fill и order — закрывают чтение кода и отчётов, а запуск и красный diff вы получаете локальным Go за пару минут.
