---
title: "ElementsMatch, Subset и Len: коллекции под контролем"
description: "Проверка слайсов в testify: ElementsMatch без порядка, кратность элементов, Subset для подмножеств, Len и Contains, ловушка nil против пустого слайса."
course: "Testify: тестирование Go с assert и require"
url: "https://golangmaster.ru/courses/testify/lessons/06-elements-match/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 18
---

# ElementsMatch, Subset и Len: коллекции под контролем

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

Проверка слайсов через assert.Equal ломается на порядке: API отдаёт те же записи в другом порядке, и тест красный при верных данных. ElementsMatch в testify сравнивает содержимое без оглядки на порядок, Subset проверяет подмножество, Len — объём. Разберём каждую, кратность элементов, слайсы структур и ловушку nil против пустого.

Тест каталога краснел после каждого релиза: записи те же, но SQL-план поменялся и выгрузка приходит в другом порядке. assert.Equal честен — он сверяет по индексам, как разобрано в уроке про [сравнение структур и слайсов](/courses/testify/lessons/05-sravnenie-struktur/), и для него [«a», «b»] и [«b», «a»] разные слайсы. Вопрос в другом: что проверяет сам тест? Если состав выгрузки — то Equal проверяет лишнее. Проверка слайсов в testify разложена по намерениям: Equal для порядка, ElementsMatch для состава, Subset для обязательных элементов, Len для объёма, Contains для точечных проверок. Отдельно — случай, когда порядок всё-таки часть контракта и как его не проглядеть. Здесь разберём их на реальном коде, посмотрим, как ElementsMatch считает кратность, сравним слайсы структур и разберём ловушку, на которой молча расходятся контракт API и его тесты: nil против пустого слайса.

### ElementsMatch: те же элементы в любом порядке

assert.ElementsMatch(t, listA, listB) проверяет, что оба аргумента — массив или слайс — содержат одни и те же элементы, порядок не важен, а кратность учитывается: если единица встречается в первом списке дважды, во втором она обязана встретиться дважды же. Документация формулирует прямо: при дубликатах число вхождений каждого элемента в обоих списках должно совпасть. Под капотом — diffLists: для каждого элемента A ищется ещё не занятый равный элемент B через ObjectsAreEqual, повторения считаются поэкземплярно, а непарные остатки попадают в отчёт как лишние и недостающие. Отчёт называется elements differ и перечисляет обе стороны — видно, кто именно потерялся, а не только факт провала. Две границы применимости. Аргументами могут быть только массив и слайс: карту ElementsMatch не примет и вежливо скажет expecting array or slice. И элементы сверяются через ObjectsAreEqual — все ловушки reflect.DeepEqual переносятся на каждый элемент списка: time.Time внутри структуры повторит свои фокусы.

### ElementsMatch на выгрузке из базы

```go
func TestCatalog(t *testing.T) {
        got := loadCatalog() // порядок строк не гарантирован

        want := []Item{
                {SKU: "book-1", Price: 500},
                {SKU: "book-2", Price: 700},
                {SKU: "pen-9", Price: 90},
        }

        assert.ElementsMatch(t, want, got)
}
```

Тест фиксирует состав выгрузки и не парится о порядке: БД вольна возвращать строки как угодно. Если запись потерялась или задублировалась, отчёт elements differ перечислит лишние и недостающие SKU. Полный прогон — локальным go test: тестовые пакеты песочница не исполняет.

Кратность — то, чем ElementsMatch отличается от «множества на глаз». Пары [«a», «a», «b»] и [«a», «b», «b»] наборы значений совпадают, а тест красный: лишняя единица не нашла пары, и отчёт покажет extra с обеих сторон. Поэкземплярный подсчёт защищает от тихих дублей в выгрузках: когда запрос начал возвращать строку дважды, тест на состав упадёт, хотя «набор» не изменился. Пустые списки свёрнуты в отдельный случай: если оба аргумента пусты — длины ноль, включая nil-слайсы — ElementsMatch проходит. Это контрастирует с Equal, который различает nil и пустое; о цене контраста — в danger-блоке ниже. Когда тест красный, а списки на вид совпадают, распечатайте обе стороны через fmt.Sprintf("%#v", x): непарный элемент обычно дубликат с невидимой разницей или значение другого типа.

### Subset и Len: граница и объём

assert.Subset(t, list, subset) проверяет, что каждый элемент subset присутствует в list — порядок и лишние элементы списка не мешают. Кратность Subset не сверяет: повтор в подмножестве требует лишь, чтобы элемент встретился хотя бы раз. Нюанс: nil в роли подмножества проходит на любом списке — testify считает nil пустым множеством. list может быть массивом, слайсом или картой — на карте ищутся ключи, а когда и список, и подмножество карты, сверяются ещё и значения по общим ключам. assert.Len(t, obj, n) требует точную длину и работает со всем, что берёт builtin len: слайс, массив, карта, строка, канал, указатель на них. На неподходящем типе отчёт скажет could not be applied builtin len(), на неверной длине — should have N item(s), but has M. Отдельный случай: на типизированном nil-слайсе Len честно вернёт ноль, а на нетипизированном nil откажется с тем же could not be applied builtin len() — слайс, даже пустой, обязан быть слайсом. Пара Len и Subset описывает форму ответа без перечисления всего содержимого: размер страницы фиксирован, обязательные разделы на месте.

### Subset и Len на ответе API

```go
func TestFeed(t *testing.T) {
        got := fetchFeed() // []string — активные разделы

        assert.Len(t, got, 4)                          // ровно четыре
        assert.Subset(t, got, []string{"news", "ads"}) // оба обязаны быть
        assert.Contains(t, got, "blog")                // и blog на месте
}
```

Len держит объём: если фид разросся на пятый раздел, тест упадёт — и это сигнал пересмотреть контракт, а не заглушить проверку. Subset гарантирует присутствие news и ads в любом порядке, Contains добивает точечную проверку. fetchFeed ходит в сеть, поэтому тест выполняется на вашей машине — в браузерной песочнице сети нет.

> **ElementsMatch не проверяет порядок.** Спорная проверка на ревью: тест рейтинга сравнивал want — три лучших по баллам — через ElementsMatch и оставался зелёным, когда сортировка сломалась: первая позиция показывала третью. Состав совпадал, порядок — нет, а для рейтинга порядок и есть контракт. ElementsMatch парит элементы, не глядя на индексы; если последовательность значима — топ-N, очередь, сортировка, вывод API — пишите Equal: он сверяет по индексам. Правило одной строкой: порядок не значит ничего — ElementsMatch; порядок значит всё — Equal.

### Слайс структур через ElementsMatch

Слайсы структур — главный рабочий случай ElementsMatch: выгрузки заказов, корзины, подписки. Элементы сверяются через ObjectsAreEqual, то есть reflect.DeepEqual по каждому, — и все ловушки прошлого урока повторяются поэлементно. time.Time в полях структур красит тест на равных слайсах: снимайте монотонные метки и выравнивайте зоны ещё в фикстурах. unexported-поля участвуют в сверке — приватный кэш в элементе сделает «одинаковые» списки разными. С указателями спокойнее, чем кажется: DeepEqual разыменовывает указатели и сравнивает значения, поэтому []*Order из разных адресов с равным содержимым пройдёт. Красным будет пара nil-элемент против указателя на нулевую структуру — внутреннее представление различается. Практическое правило: чем больше в элементах непрозрачных типов, тем ближе момент, когда тест на состав стоит разбить на Len плюс сверку ключевых полей по отдельности.

### Дубликаты в корзине

```go
func TestBasket(t *testing.T) {
        got := basketItems() // []*Item в произвольном порядке

        want := []*Item{
                {SKU: "book-1", Qty: 2},
                {SKU: "book-1", Qty: 2}, // дважды: кратность важна
                {SKU: "pen-9", Qty: 1},
        }

        assert.ElementsMatch(t, want, got)
}
```

Кратность работает и на структурах: каждая из двух одинаковых позиций занимает свой экземпляр в сверке. Если вторая book-1 потерялась, отчёт elements differ покажет её в недостающих. Корзина не обещает порядка позиций, зато обещает состав и количество — ровно то, что сверяет ElementsMatch.

> **nil против пустого: контракт, который проверял сам себя.** JSON «null» разворачивается в nil-слайс, «[]» — в пустой. assert.Equal(t, []Item{}, got) красен на nil-слайсе: DeepEqual различает внутреннее представление, хотя длина у обоих ноль. ElementsMatch на этой паре зелёный — оба считаются пустыми. Оборотная сторона: тест на состав пройдёт, когда сериализация начнёт выдавать null вместо [] — а фронтенду это другой контракт, и прод покажет это раньше тестов. Лекарство — договориться о форме и нормализовать на границе: if got == nil { got = []Item{} }, после чего сравнивать чем угодно.

### Contains: строка, карта, слайс

assert.Contains универсален до хитрости: поведение зависит от типа контейнера. Строка — ищется подстрока, внутри работает strings.Contains: Contains(t, "catalog", "alog") зелёный. Слайс и массив — ищется элемент через ObjectsAreEqual, включая структуры. Карта — ищется КЛЮЧ через MapIndex, значения карты Contains не проверяет никогда. Отсюда классическая ошибка: Contains(t, map[string]string{"a": "x"}, "x") краснеет — среди ключей «x» нет. Для значений достаньте их в слайс или сравните карту целиком через Equal. NotContains зеркален на всех трёх ветках: NotContains(t, m, "deleted") зелёный, пока ключ deleted не появился, — удобный сторож против протёкших внутренних статусов в публичном ответе. В связке с Len и Subset Contains закрывает быстрый контроль формы ответа: объём известен, обязательные элементы на месте, запрещённого нет.

### Пять проверок коллекций

| Проверка | Что гарантирует | Типичное место |
| --- | --- | --- |
| assert.Equal | тот же состав в том же порядке | сортировка, топ-N, сериализация |
| assert.ElementsMatch | тот же состав и кратность, порядок любой | выгрузки из БД, неупорядоченные ответы |
| assert.Subset | каждый элемент подмножества присутствует | обязательные разделы в ответе |
| assert.Len | точное число элементов | пагинация, размер страницы |
| assert.Contains | элемент слайса, ключ карты, подстрока | точечная проверка без перечисления |

### Какую проверку слайса выбрать

- Порядок значим — assert.Equal: сверка по индексам, дифф покажет первое расхождение.
- Порядок не значим, состав и кратность значимы — assert.ElementsMatch.
- Нужно только присутствие обязательных элементов — assert.Subset, кратность он не трогает.
- Нужен только объём — assert.Len: дешёвая проверка формы ответа.
- Один элемент, ключ или подстрока — assert.Contains.
- nil и пустое различны для Equal и совпадают для ElementsMatch и Len(t, x, 0): нормализуйте слайс на границе пакета.

### Проверка: кратность

**Вопрос:** assert.ElementsMatch(t, []int{1, 1, 2}, []int{1, 2, 2}) — что произойдёт?

- Пройдёт: наборы значений совпадают
- Пройдёт: кратность ElementsMatch не интересует
- ✅ Упадёт: у единицы кратность 2 против 1, у двойки 1 против 2
- Упадёт: ElementsMatch не работает с числами

Число вхождений каждого элемента обязано совпасть: непарная единица и непарная двойка уйдут в отчёт elements differ с обеих сторон. Совпадение набора значений — не то же самое, что совпадение состава.

### Проверка: nil и пустое

**Вопрос:** got — nil-слайс []Item(nil). Какие проверки пройдут?

- assert.Equal(t, []Item{}, got) и assert.Len(t, got, 0)
- ✅ assert.Len(t, got, 0) и assert.ElementsMatch(t, []Item{}, got)
- только assert.Equal: nil считается пустым
- ни одна: nil-слайс нельзя сравнивать

У nil-слайса длина 0 — Len зелёный; оба списка пусты — ElementsMatch зелёный. Equal красный: DeepEqual различает nil и пустой слайс по внутреннему представлению, даже когда длины совпадают.

### Что покажет прогон?

```go
func TestFeed(t *testing.T) {
        got := []string{"b", "a", "c"}

        assert.Equal(t, []string{"a", "b", "c"}, got)
        assert.ElementsMatch(t, []string{"a", "b", "c"}, got)
}
```

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

- Обе зелёные: содержимое совпадает
- ✅ Equal красный, ElementsMatch зелёный
- Equal зелёный, ElementsMatch красный
- Обе красные

Equal сверяет по индексам: на первой позиции ждёт «a», видит «b» — красный. ElementsMatch парит элементы независимо от индексов и находит все три пары — зелёный. Вся разница проверок в отношении к порядку.

### Соберите контроль фида

Допишите три проверки: точный объём, обязательные элементы и один точечный элемент.

```go
func TestFeed(t *testing.T) {
        got := fetchFeed()

        assert.___(t, got, 3)                        // ровно три записи
        assert.___(t, got, []string{"news", "blog"}) // обе обязаны быть
        assert.___(t, got, "docs")                   // и docs на месте
}
```

Подсказка: Объём меряет Len, присутствие списка элементов гарантирует Subset, один элемент ищет Contains.

Правильные ответы в пропусках: `Len`, `Subset`, `Contains`

### Соберите тест на состав каталога

Расставьте строки: загрузка, эталон, проверка состава и контроль объёма.

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

1. `func TestCatalog(t *testing.T) {`
2. `got := loadCatalog()`
3. `want := []Item{{SKU: "book-1"}, {SKU: "pen-9"}}`
4. `assert.ElementsMatch(t, want, got)`
5. `assert.Len(t, got, 2)`
6. `}`

### Карточки: коллекции под контролем

- **assert.ElementsMatch** — Те же элементы в любом порядке, кратность поэкземплярно; аргументы — только массив или слайс; два пустых списка, включая nil, проходят.
- **assert.Subset** — Каждый элемент подмножества присутствует в списке; кратность не сверяется; nil-подмножество проходит всегда; список — массив, слайс или карта (по ключам).
- **assert.Len** — Точная длина всего, что берёт builtin len: слайс, массив, карта, строка, канал. Отчёт при провале: should have N item(s), but has M.
- **assert.Contains** — Строка — подстрока, слайс — элемент, карта — ключ. Значения карты Contains не ищет; NotContains зеркален на всех ветках.
- **nil против пустого слайса** — Equal различает представление — красный; ElementsMatch и Len(t, x, 0) считают оба пустыми — зелёный. JSON null против [] — вопрос контракта API.
- **ElementsMatch или Equal** — Порядок не значит ничего — ElementsMatch; порядок часть контракта (топ-N, сортировка, очередь) — Equal.

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

Состав, порядок и объём под контролем — остаются числа, где Equal подводит уже по другой причине: 0.1+0.2 не равно 0.3, и тест на метрики краснеет без вины кода. Урок про [InDelta и InEpsilon](/courses/testify/lessons/07-in-delta/) разбирает дельты и допуски. Дальше — простые проверки на каждый день: [True, False, Nil и Empty](/courses/testify/lessons/08-bool-nil-empty/), где пустота слайса превращается из ловушки в отдельную проверку. Примеры урока прогоняются локальным go test, как и весь курс: песочница исполняет фрагменты логики, но тестовые пакеты с зависимостью testify требуют терминала. Карта всех двадцати пяти тем — на странице [курса по testify](/courses/testify/).

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

- ElementsMatch сверяет состав без порядка: те же элементы с той же кратностью, любые индексы.
- Кратность считается поэкземплярно: лишний дубль или потерянная запись красят тест, совпадение набора значений не спасает.
- Subset гарантирует присутствие каждого элемента подмножества и не следит за кратностью; nil-подмножество проходит всегда.
- Len требует точную длину и работает со всем, что берёт builtin len: слайс, массив, карта, строка, канал.
- Contains ищет подстроку в строке, элемент в слайсе и ключ в карте — значения карты остаются невидимы.
- Порядок не значим — ElementsMatch; порядок значим — Equal: зелёный тест рейтинга на ElementsMatch — брак проверки.
- nil и пустой слайс равны для ElementsMatch и Len и различны для Equal: нормализуйте форму на границе пакета.

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

Добавьте в свой тестовый пакет две проверки одной выгрузки: Equal на отсортированный слайс и ElementsMatch на несортированный. Затем разверните в JSON сначала [] и null — и посмотрите, какая проверка различает nil и пустое. Нормализуйте слайс на границе пакета и прогоните go test локально.

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

### ElementsMatch проверяет порядок элементов?

Нет: он парит элементы независимо от индексов и сверяет кратность поэкземплярно. Порядок для него не существует. Если последовательность — часть контракта, как у топ-N, сортировки или очереди вывода, пишите assert.Equal: он сравнивает по индексам и покажет первое расхождение.

### Чем Subset отличается от ElementsMatch?

ElementsMatch требует полного совпадения состава с обеих сторон: лишние элементы в списке красят тест. Subset проверяет одно направление — каждый элемент подмножества обязан найтись в списке, а лишнее в списке допустимо. Кратность Subset тоже не сверяет: повтор в подмножестве требует лишь одного вхождения.

### Почему ElementsMatch зелёный на nil и пустом слайсе, а Equal красный?

ElementsMatch сворачивает случай двух пустых списков заранее: нулевые длины, включая nil, считаются совпадающими. Equal уходит в reflect.DeepEqual, который различает внутреннее представление nil-слайса и пустого. Если форма JSON-ответа — контракт, нормализуйте слайс на границе пакета и зафиксируйте её отдельным тестом.
