День 17 · Срезы изнутри: len, cap и общий массив · страница 6 из 7

Тест: функция не трогает вход

В дне 14 тест сравнивал число с числом. Функции над срезами возвращают срезы, и у них есть второе обещание: входной срез после вызова прежний. Сегодня тест проверяет оба.


Срезы в тесте сравнивают через slices.Equal

got != []int{12} в тесте не соберётся — ни один срез, кроме nil, так не сравнивают:

Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day17/scratch$ go test .
# day17/scratch [day17/scratch.test]
./main_test.go:7:5: invalid operation: got != []int{…} (slice can only be compared to nil)
FAIL	day17/scratch [build failed]
FAIL

Сравнивают функцией со страницы 5: if !slices.Equal(got, want).


Фильтр на месте

Ситуация. В чужом коде часто встречается такой фильтр «убрать пустые ячейки» — без нового массива:

Код для чтения · разбираем, набирать не нужно
package main

import "fmt"

func withoutZero(stock []int) []int {
	res := stock[:0]
	for _, q := range stock {
		if q != 0 {
			res = append(res, q)
		}
	}
	return res
}

func main() {
	stock := []int{0, 12, 0, 7, 5}
	full := withoutZero(stock)
	fmt.Println(full)
	fmt.Println(stock)
}

Что увидите.

Вывод · это печатает программа, набирать не нужно
[12 7 5]
[12 7 5 7 5]

Как это читать. stock[:0] — окно нулевой длины на тот же массив, ёмкость 5. Каждый append в res пишет в начало массива stock:

Образец · набирать не нужно
 до цикла:  ┌────┬────┬────┬────┬────┐   res: len 0 cap 5
            │  0 │ 12 │  0 │  7 │  5 │
            └────┴────┴────┴────┴────┘
 после:     ┌────┬────┬────┬────┬────┐   res: len 3 cap 5
            │ 12 │  7 │  5 │  7 │  5 │
            └────┴────┴────┴────┴────┘
            └──── res ─────┘

Это те же «два среза на один массив»: результат верный, а вход испорчен. Приём законен, когда вход больше не нужен, и вреден, когда вызывающий им ещё пользуется.


Тест, который это пропускает, и тест, который ловит

Тест по образцу дня 14 проверяет только результат:

Код для чтения · разбираем, набирать не нужно
package main

import (
	"slices"
	"testing"
)

func TestWithoutZero(t *testing.T) {
	stock := []int{0, 12, 0, 7, 5}
	got := withoutZero(stock)
	want := []int{12, 7, 5}
	if !slices.Equal(got, want) {
		t.Errorf("withoutZero = %v, хотели %v", got, want)
	}
}
Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day17/scratch$ go test .
ok  	day17/scratch	0.006s

Зелёный — а функция портит вход. Добавим второе сравнение: сам stock после вызова против его прежнего содержимого, записанного отдельным литералом.

Код для чтения · разбираем, набирать не нужно
package main

import (
	"slices"
	"testing"
)

func TestWithoutZero(t *testing.T) {
	stock := []int{0, 12, 0, 7, 5}
	got := withoutZero(stock)
	want := []int{12, 7, 5}
	if !slices.Equal(got, want) {
		t.Errorf("withoutZero = %v, хотели %v", got, want)
	}
	before := []int{0, 12, 0, 7, 5}
	if !slices.Equal(stock, before) {
		t.Errorf("входной срез изменился: %v, был %v", stock, before)
	}
}
Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day17/scratch$ go test .
--- FAIL: TestWithoutZero (0.00s)
    main_test.go:17: входной срез изменился: [12 7 5 7 5], был [0 12 0 7 5]
FAIL
FAIL	day17/scratch	0.005s
FAIL

Теперь тест красный. Починка — одна строка: var res []int вместо res := stock[:0], и у результата свой массив. После неё тот же тест печатает ok.

Две детали, без которых проверка входа не работает:

%v в t.Errorf печатает срез так же, как Println: в квадратных скобках.


Попробуйте сейчас: задание keep — тест на результат и на вход.

Цель: nonempty_test.go в ~/gocourse/day17/keep падает и на функции, которая портит вход, и на функции с неверным результатом.

1. Прочитайте условие и функцию:

▶ Выполните
cd ~/gocourse/day17/keep
cat TASK.txt
cat nonempty.go
go run .

2. Наберите nonempty_test.go по образцу TestWithoutZero выше. Во входе — ячейка ровно с minQty шт., ячейка меньше minQty и пустая ячейка раньше полной. Числа считайте от константы: minQty + 10, minQty - 1 — тогда тест не зависит от вашего числа.

3. Проверьте силу теста приёмом дня 14: временно замените в nonempty.go строку var res []int на res := stock[:0], запустите go test . — тест обязан упасть на проверке входа — и верните строку.

4. Проверьте и закоммитьте:

▶ Выполните
go test -v .
diff ~/.course/orig/day17/keep/nonempty.go nonempty.go
git add .
git commit -m "keep: тест на результат и на вход"

Готово, когда: go test . печатает ok на исходной функции, diff пуст, пункты keep и keepres зелёные. Проверка подменит функцию двумя неверными версиями — одна портит вход, другая ставит ноль в ячейку ровно в порог, не меняя длины результата, — и ещё одной верной, записанной иначе.


Что может пойти не так

Что видите Что это значит Что делать
invalid operation: got != []int{…} (slice can only be compared to nil) и [build failed] срезы сравнивают оператором !slices.Equal(got, want)
undefined: slices в файле теста у файла теста свой import добавить "slices" в import теста
тест зелёный на функции, которая портит вход вход после вызова не сверяется второе slices.Equal: stock против литерала
проверка входа есть, а порча не ловится before := stock — окно на тот же массив, или пустые ячейки стоят только в конце входа прежнее содержимое — отдельным литералом, пустую ячейку — первой

Дальше: трассировка с колонками len и cap — course next