Property-based, mutation, load и chaos testing в Go
Зачем знать. Юнит-тесты ловят только то, что разработчик подумал проверить. Middle 2 Go-инженер должен уметь дополнять их property-based (генеративные инварианты), оценивать качество тестов через mutation testing, нагружать сервис через load testing и проверять устойчивость к сбоям через chaos engineering. Это уровни тестов, без которых production-системы ломаются на ровном месте.
Содержание
Заголовок раздела «Содержание»- Концепции четырёх дисциплин
- Production-практики и инструменты
- Gotchas
- Real cases
- 25 вопросов
- Practice
- Источники
1. Концепции (кратко)
Заголовок раздела «1. Концепции (кратко)»1.1 Property-based testing
Заголовок раздела «1.1 Property-based testing»Идея: вместо «для входа X ожидаем Y» (example-based) — описать свойство, которое должно держаться для любого input, а фреймворк генерирует входы автоматически.
Классические свойства:
Reverse: reverse(reverse(xs)) == xs для любого xsSort: is_sorted(sort(xs)) для любого xsJSON: decode(encode(x)) == x для любого x (round-trip)Append: len(append(xs, y)) == len(xs) + 1 для любого xs, yMath: |a + b| <= |a| + |b| triangle inequalityIdempotent: hash(x) == hash(x) стабильноGenerator строит случайные input заданного типа; shrinker при провале сжимает до минимального contre-example (легче дебажить).
1.2 Mutation testing
Заголовок раздела «1.2 Mutation testing»Идея: исказить (mutate) кусок кода — изменить > на >=, + на -, удалить вызов, инвертировать boolean. Запустить тесты. Если тесты всё ещё «pass» — значит они не покрывают эту мутацию, есть пробел.
Метрика: mutation score = killed / total_mutants. Чем выше, тем сильнее ваш suite.
1.3 Load testing
Заголовок раздела «1.3 Load testing»Идея: нагрузить систему до её предельных режимов, измерить RPS, latency (p50/p95/p99), error rate. Различают:
- Closed-loop: фиксированное число клиентов (как пользователи в браузере).
- Open-loop: фиксированный rate новых запросов (как событийный поток).
- Stress vs Spike vs Soak (длительный) — разные сценарии.
1.4 Chaos engineering
Заголовок раздела «1.4 Chaos engineering»Идея (Netflix, 2010): «нельзя верить тому, что не сломано». Преднамеренно вносим failure (kill процесса, latency сети, OOM, partition) и проверяем, что система продолжает работать (стационарное состояние не нарушено).
Принципы (Chaos Engineering Principles):
- Сформулировать steady state hypothesis.
- Варьировать события реального мира.
- Запускать эксперименты в production (когда команда зрелая).
- Автоматизировать.
- Минимизировать blast radius.
2. Production-практики
Заголовок раздела «2. Production-практики»2.1 Property-based в Go: библиотеки
Заголовок раздела «2.1 Property-based в Go: библиотеки»| Библиотека | Статус | Когда |
|---|---|---|
testing/quick (stdlib) | очень минимальный, без shrinking | для простых case |
github.com/leanovate/gopter | классика, активная, mature | основной выбор до 2022 |
pgregory.net/rapid | современный, отличный shrinker, recommend | рекомендация 2025+ |
Пример: rapid
Заголовок раздела «Пример: rapid»import "pgregory.net/rapid"
func TestSortIdempotent(t *testing.T) { rapid.Check(t, func(t *rapid.T) { xs := rapid.SliceOf(rapid.Int()).Draw(t, "xs") ys := append([]int(nil), xs...) sort.Ints(ys) sort.Ints(ys) // повторно zs := append([]int(nil), xs...) sort.Ints(zs) require.Equal(t, zs, ys, "sort должен быть идемпотентен") })}
func TestRoundtripJSON(t *testing.T) { rapid.Check(t, func(t *rapid.T) { order := genOrder(t) // custom generator b, err := json.Marshal(order) require.NoError(t, err) var back Order require.NoError(t, json.Unmarshal(b, &back)) require.Equal(t, order, back) })}
func genOrder(t *rapid.T) Order { return Order{ ID: rapid.StringMatching(`[0-9a-f]{8}`).Draw(t, "id"), Total: rapid.IntRange(0, 1_000_000).Draw(t, "total"), Items: rapid.SliceOfN(rapid.String(), 1, 10).Draw(t, "items"), }}Пример: gopter
Заголовок раздела «Пример: gopter»import ( "github.com/leanovate/gopter" "github.com/leanovate/gopter/gen" "github.com/leanovate/gopter/prop")
func TestReverse(t *testing.T) { p := gopter.NewProperties(nil) p.Property("reverse(reverse(xs)) == xs", prop.ForAll( func(xs []int) bool { return reflect.DeepEqual(reverse(reverse(xs)), xs) }, gen.SliceOf(gen.Int()), )) p.TestingRun(t)}Что генерить (паттерны)
Заголовок раздела «Что генерить (паттерны)»- Round-trip: encode→decode (JSON, Avro, Protobuf), gzip→gunzip.
- Инвариант: после операции свойство держится (BST остался отсортированным).
- Эквивалентность реализаций: новая (быстрая) ≡ старой (медленной).
- Идемпотентность: f(f(x)) == f(x) (нормализация).
- Метаморфические соотношения: f(2x) ≈ 2·f(x) (для аналитических функций).
Shrinker
Заголовок раздела «Shrinker»При провале фреймворк автоматически уменьшает input. Пример:
- найдено:
xs=[42, -17, 88, 1, 1000, -5]ломает тест. - shrinker: попробует
[],[42],[1000],[42, 88]… - финал:
xs=[1000]— минимальный contre-example.
⚠️ Сложные generator’ы требуют custom shrinker для понятных contre-example.
2.2 Property testing + fuzzing (Go 1.18+)
Заголовок раздела «2.2 Property testing + fuzzing (Go 1.18+)»Go добавил встроенный fuzzing go test -fuzz:
func FuzzParse(f *testing.F) { f.Add([]byte(`{"a": 1}`)) f.Fuzz(func(t *testing.T, data []byte) { v, err := Parse(data) if err != nil { return } // round-trip b, _ := Marshal(v) v2, err := Parse(b) if err != nil { t.Fatal("re-parse failed") } if !reflect.DeepEqual(v, v2) { t.Fatal("round-trip mismatch") } })}go test -fuzz=FuzzParse -fuzztime=30sРазличия:
- Fuzzing — coverage-guided, ищет crashes; работает с
[]byte/примитивами. - Property-based (rapid) — описание свойств для типизированных значений; не coverage-guided, но богаче.
- Часто комбинируют: fuzz → ловит панику; property → проверяет инвариант.
2.3 Mutation testing в Go: gremlins
Заголовок раздела «2.3 Mutation testing в Go: gremlins»
github.com/go-gremlins/gremlins— основной инструмент для Go (2025+).
Установка и запуск:
go install github.com/go-gremlins/gremlins/cmd/gremlins@latestgremlins run ./...Pipeline:
- gremlins парсит AST, идентифицирует операторы.
- Для каждого создает «мутанта» (например
>→>=). - Компилирует, запускает тесты.
- Если тест fall — мутант killed, иначе survived (тестов не хватает).
Типы мутаций:
- Arithmetic:
+,-,*,/swap. - Conditional:
>↔<,==↔!=. - Increment/Decrement:
++↔--. - Constants:
true↔false,0↔1. - Return values:
return x↔return zero/nil.
Пример отчёта:
Mutator Killed Survived ScoreARITHMETIC_BASE 24 3 88%CONDITIONALS_BOUND 12 1 92%INCREMENT_DECREMENT 6 0 100%─────────────────────────────────────────────Total 42 4 91%Survived mutants — место для усиления тестов: написать пример, где разница между исходным кодом и мутантом видна.
⚠️ Mutation testing медленный: для каждого мутанта прогон тестов. Гремлины умеют параллелить + использовать только тесты для конкретного пакета.
2.4 Load testing: инструменты
Заголовок раздела «2.4 Load testing: инструменты»| Инструмент | Язык | Сильные стороны | Когда |
|---|---|---|---|
| vegeta | Go | constant rate (open-loop), CLI, простой | quick http бенчмарк |
| k6 | JS-сценарий + Go-engine | scripted, mature reporting, Grafana | сложные сценарии |
| wrk2 | C | constant throughput (правильно), latency | строгий p99 |
| ghz | Go | gRPC специфично | для gRPC |
| ApacheBench (ab) | C | очень простой | один endpoint, dirty test |
| Locust | Python | scripted, distributed | если уже Python в экосистеме |
| Gatling | Scala | enterprise, отчёты | JVM-команды |
vegeta — пример
Заголовок раздела «vegeta — пример»echo "GET http://localhost:8080/api/orders" | \ vegeta attack -rate=100/s -duration=30s | \ tee results.bin | vegeta report
# гистограммаvegeta report -type=hist[0,10ms,50ms,100ms,500ms,1s] < results.bin
# HTML отчётvegeta report -type=plot < results.bin > plot.htmlVegeta — open-loop (constant rate), что правильно моделирует трафик пользователей; не зависит от latency сервиса (в отличие от ab, который выдаёт rate ~= 1/latency).
k6 — пример
Заголовок раздела «k6 — пример»import http from 'k6/http';import { check, sleep } from 'k6';
export const options = { scenarios: { ramp_up: { executor: 'ramping-vus', startVUs: 0, stages: [ { duration: '1m', target: 100 }, { duration: '5m', target: 100 }, { duration: '1m', target: 0 }, ], }, }, thresholds: { http_req_duration: ['p(95)<200', 'p(99)<500'], http_req_failed: ['rate<0.01'], },};
export default function () { const r = http.get('http://localhost:8080/api/orders'); check(r, { 'status 200': (r) => r.status === 200 }); sleep(0.1);}k6 run script.jsk6 run --out influxdb=http://localhost:8086/k6 script.jsghz — gRPC
Заголовок раздела «ghz — gRPC»ghz --insecure \ --proto=order.proto --call=order.Service.Create \ -d '{"customer_id": 1}' \ -c 50 -n 10000 \ localhost:500512.5 Closed-loop vs Open-loop
Заголовок раздела «2.5 Closed-loop vs Open-loop»Open-loop (vegeta, wrk2):
- Каждую секунду создаёт N новых запросов независимо от ответа предыдущих.
- Корректно моделирует входящий event-flow.
- Если сервис тормозит — queue запросов растёт, latency × 100.
Closed-loop (ab, jmeter по умолчанию):
- N клиентов в цикле: «отправил — дождался ответа — отправил снова».
- Скорость =
N / latency. Если сервис тормозит — клиенты тоже тормозят. - Маскирует деградацию: «вижу 1000 RPS» при p99=10s.
⚠️ Closed-loop искажает результаты. Для realistic load — open-loop. Это знаменитая «coordinated omission» проблема (Gil Tene).
2.6 Warm-up и realistic shaping
Заголовок раздела «2.6 Warm-up и realistic shaping»- Warm-up: 30-60 секунд лёгкой нагрузки, чтобы JIT/cache/connection pool прогрелся. Пер-период измерения отдельно от warm-up.
- Ramp: постепенный набор RPS (0→target за минуту).
- Soak: 1-24 часа на target нагрузке (ловим memory leak, GC degradation).
- Spike: внезапный 5x burst (моделирует продуктовые акции).
2.7 Chaos engineering: инструменты
Заголовок раздела «2.7 Chaos engineering: инструменты»| Инструмент | Платформа | Что делает |
|---|---|---|
| Chaos Mesh | Kubernetes (CNCF) | pod kill, network, IO, time chaos |
| LitmusChaos | Kubernetes (CNCF) | workflows, gameday-сценарии |
| Chaos Toolkit | universal | open-source движок, плагины |
| Gremlin (commercial) | universal | managed, GUI |
| toxiproxy | TCP-уровень | latency / disconnect между app и зависимостями |
| Pumba | Docker | network/process chaos для контейнеров |
| chaos-monkey | AWS (Spinnaker) | random instance kill |
Failure modes (карта)
Заголовок раздела «Failure modes (карта)»| Категория | Примеры |
|---|---|
| Process | kill -9, OOM, panic |
| Network | latency, packet loss, partition, DNS fail |
| Resource | CPU stress, memory exhaust, disk full, fork-bomb |
| Time | clock skew, NTP-jump |
| Dependencies | DB unavailable, slow query, broker disconnect |
| State | corrupted data, partial write |
Chaos Mesh: пример NetworkChaos
Заголовок раздела «Chaos Mesh: пример NetworkChaos»apiVersion: chaos-mesh.org/v1alpha1kind: NetworkChaosmetadata: name: orders-latencyspec: action: delay mode: one selector: labelSelectors: "app": "orders" delay: latency: "200ms" correlation: "100" jitter: "50ms" duration: "5m"Применит 200ms задержку к 1 поду orders на 5 минут.
toxiproxy в Go-тестах
Заголовок раздела «toxiproxy в Go-тестах»import "github.com/Shopify/toxiproxy/v2/client"
toxiClient := toxiproxy.NewClient("localhost:8474")proxy, _ := toxiClient.CreateProxy("pg", "0.0.0.0:25432", "real-pg:5432")proxy.AddToxic("slow", "latency", "downstream", 1.0, toxiproxy.Attributes{ "latency": 500, "jitter": 100,})
// тесты теперь ходят на localhost:25432 → toxiproxy → real-pg c 500ms latencyОчень полезно в integration-тестах для проверки timeouts/retries.
2.8 Game days
Заголовок раздела «2.8 Game days»Game day — учения: команда в офис-час собирается, запускает chaos-эксперимент в проде (или почти проде), мониторит, фиксит. Цели:
- проверить runbooks (а можем ли мы реально восстановить);
- найти неочевидные dependencies;
- обучить новичков.
Минимальный сценарий: kill-9 одного pod в проде → SLO держится? оповещения сработали? сколько 5xx видел юзер?
2.9 Steady state hypothesis
Заголовок раздела «2.9 Steady state hypothesis»Формулировка до эксперимента:
«Latency p99 < 500ms, error rate < 0.1%, success rate >= 99.9% для
/api/ordersв течение эксперимента.»
Если во время chaos гипотеза держится — система устойчива. Если нарушена — есть слабое место.
2.10 Blast radius
Заголовок раздела «2.10 Blast radius»Начинай с минимума:
- One pod в staging.
- → One pod в prod (canary).
- → Multiple pods staging.
- → Multiple pods prod.
- → AZ failure (если осилит).
Никогда не лезь сразу «убить весь регион».
3. Gotchas
Заголовок раздела «3. Gotchas»⚠️ Property tests без shrinker дают огромные contre-example. Падает с xs=[1942, -17, 88, 1, 0, ...500 элементов] — непонятно где баг. Используй rapid или пиши custom shrinker.
⚠️ Custom generator может не покрывать edge cases. Если genInt = IntRange(0, 100) — никогда не тестируется отрицательное число. Думай про границы (MaxInt, 0, nil, "").
⚠️ Property test, который иногда падает — flake. Сохраняй seed для воспроизведения (rapid печатает seed).
⚠️ Mutation testing exponentially slow. 1000 файлов × 50 мутантов × 2 секунды на test suite = 27 часов. Используй --threshold и фильтры по changed-файлам.
⚠️ Survived mutants — не всегда баг тестов. Могут быть equivalent mutants — изменения кода, которые семантически эквивалентны (например i < 100 ↔ i <= 99). Их нельзя «убить».
⚠️ Load test против localhost ничего не показывает. TCP-стек, network latency, OS-tuning — всё локально нерелевантно. Делай против stage с реальным сетевым путём.
⚠️ Closed-loop benchmark маскирует degradation. «Apache Benchmark показывает 1000 RPS» при p99=10s — это плохо, а не хорошо.
⚠️ Warm-up забывают, и p99 «грязный». Первые 30 секунд — connection pool + JIT + cache — отдельный buckets.
⚠️ Coordinated omission: измеритель ждёт ответа и не отправляет новые запросы → пропускает latency. wrk2 и vegeta его учитывают; ab — нет.
⚠️ Chaos в проде без мониторинга = безответственность. Сначала observability (метрики, алерты, runbook), потом chaos.
⚠️ «Chaos Monkey» в стартапе — обычно вред, а не польза. Сначала надёжность базовой системы. Chaos — после нескольких 9.
⚠️ toxiproxy в production не нужен. Это test-tool. Для prod-chaos используй Chaos Mesh / LitmusChaos.
⚠️ Mocked dependency в property test — теряется ценность. Property-тесты лучше всего работают над «чистой логикой». Для интегрированных property — реальная зависимость (testcontainers).
⚠️ Fuzz-инпуты не сохраняются автоматически в git — Go хранит их в testdata/fuzz/.... Коммить — иначе разработчик не воспроизведёт.
⚠️ k6 в Go-проектах — JavaScript-сценарии. Это не «meta» в Go-стеке, но дешевле, чем писать своё.
⚠️ Игнорирование observability во время load test. Графики throughput без CPU/GC/p99 не дают ответа «почему деградирует».
4. Real cases
Заголовок раздела «4. Real cases»4.1 JSON-парсер — property + fuzz
Заголовок раздела «4.1 JSON-парсер — property + fuzz»Команда написала custom JSON-парсер. Покрытие unit — 95%. Property-test (encode → decode → encode' равенство) + fuzz нашёл 12 багов: NaN не сериализуется обратно, escape Unicode surrogate, отрицательный zero, etc. Mutation score вырос с 78% до 94%.
4.2 Mutation revealed dead test
Заголовок раздела «4.2 Mutation revealed dead test»Прогон gremlins на сервисе показал что 30% мутантов выживают в модуле pricing. Расследование: «тест» был t.Skip("flaky") несколько месяцев. Исправили, mutation score вырос до 88%.
4.3 Load test нашёл deadlock
Заголовок раздела «4.3 Load test нашёл deadlock»vegeta 200 RPS, soak 1 час → к 30-й минуте p99 ушёл с 50ms на 30s. Pprof показал deadlock в connection pool. Ни один unit/integration не воспроизводил, потому что не было длительной нагрузки.
4.4 Chaos Mesh: разорвал partition
Заголовок раздела «4.4 Chaos Mesh: разорвал partition»Эксперимент: кросс-AZ partition между app и Postgres replica. Гипотеза: failover на другой replica в 30s. Реально: 4 минуты (DNS-кэш). Зафиксили — добавили health-check + DNS TTL=5.
4.5 Netflix Simian Army
Заголовок раздела «4.5 Netflix Simian Army»Pioneer chaos engineering: Chaos Monkey (kill instances), Latency Monkey (delay), Chaos Gorilla (AZ), Chaos Kong (region). Сейчас интегрировано в Spinnaker.
5. Вопросы
Заголовок раздела «5. Вопросы»1. Что такое property-based testing? Тестирование, где описывают свойство (инвариант), которое должно держаться для любого входа; framework генерирует случайные входы автоматически.
2. Чем property test отличается от example-based? Example: «для X получи Y». Property: «для любого x: P(x) истина». Больше покрытия, ловит edge cases.
3. Какие свойства типично проверяют? Round-trip (encode→decode), идемпотентность, эквивалентность реализаций, инварианты структур (BST остался отсортированным).
4. Что такое shrinker? При провале теста — алгоритм уменьшает input до минимального contre-example, удобного для дебага.
5. Какие библиотеки property-testing в Go?
testing/quick (stdlib, minimal), gopter (классика), pgregory.net/rapid (recommended 2025+).
6. Чем rapid лучше gopter?
Современный API, лучший shrinker, активная разработка, лучшие custom generators.
7. Чем property test отличается от fuzzing?
Fuzz — coverage-guided ищет crash на []byte. Property — описывает инвариант на типизированных данных. Можно комбинировать.
8. Что такое mutation testing? Метод оценки качества тестов: исказить код (mutate), проверить что тесты падают; survived mutant = тест не покрывает.
9. Какой инструмент в Go?
go-gremlins/gremlins — основной.
10. Что такое mutation score? Доля убитых мутантов: killed / total. Высокий score = тесты сильные.
11. Что такое equivalent mutant?
Изменение кода, семантически эквивалентное оригиналу; не может быть «убит» (например i<100 ↔ i<=99 при integer).
12. Закладные ограничения mutation testing? Медленный (N × прогон тестов); equivalent mutants создают «ложные» survived; шумит на сложной логике.
13. Что такое load testing? Нагрузка на сервис до предельных режимов; измерение throughput, latency, error rate.
14. Closed-loop vs Open-loop? Closed: N клиентов в цикле, RPS зависит от latency. Open: фиксированный rate, не зависит от ответа. Open — корректнее.
15. Что такое coordinated omission? Артефакт benchmark-инструментов, где замедление сервиса маскируется (измеритель ждёт и не отправляет новые запросы). wrk2 и vegeta это учитывают.
16. Какие инструменты load testing для Go? vegeta (CLI, Go), k6 (JS-сценарий, Go-engine), ghz (gRPC), wrk2 (C, constant throughput).
17. Что такое warm-up? Период до измерения (30-60s): прогрев JIT, connection pool, cache. Не входит в результаты.
18. Что такое soak test? Длительная (1-24h) нагрузка target-уровня; ловит memory leak, GC degradation, file descriptor leak.
19. Что такое chaos engineering? Дисциплина: целенаправленно ломать систему в production-like условиях, чтобы найти слабые места до реальных инцидентов.
20. Принципы chaos engineering?
- Steady state hypothesis. 2) Vary real-world events. 3) Run in production (когда зрело). 4) Automate. 5) Minimize blast radius.
21. Какие классы failure? Process (kill, OOM), Network (latency, partition), Resource (CPU/mem/disk), Time (clock skew), Dependencies (DB down).
22. Инструменты chaos engineering? Chaos Mesh (K8s, CNCF), LitmusChaos (K8s, CNCF), Chaos Toolkit, Gremlin (commercial), toxiproxy (TCP).
23. Что такое game day? Учения команды: запускают chaos-эксперимент, мониторят, фиксят runbooks. Цель — обучение и валидация процедур.
24. Что такое blast radius? Объём ущерба от эксперимента. Минимизируется (1 pod → AZ → region) поэтапно по мере уверенности.
25. Зачем toxiproxy в integration-тестах? Симулирует latency/disconnect между app и зависимостью на TCP-уровне; полезно проверять timeouts, retries, circuit breakers.
6. Practice
Заголовок раздела «6. Practice»-
Round-trip property. Напиши custom JSON-encoder/decoder для нескольких типов; rapid-тест:
decode(encode(x)) == xдля случайных values. -
Sort idempotence. rapid-тест на собственную sort-функцию:
sort(sort(xs)) == sort(xs)+is_sorted(sort(xs)). -
Property + fuzz combo. Напиши
FuzzParseдля парсера +TestPropertyParseдля инварианта. Сравни найденные баги. -
Custom generator + shrinker. Сгенерируй валидный JWT-token (header+payload+signature) для тестов — научи shrinker сжимать payload.
-
gremlins на pet-проект. Прогоняй mutation testing на свой Go-проект; идентифицируй survived mutants; усиль тесты.
-
vegeta benchmark. Снагрузи свой HTTP-сервис:
100/s,500/s,1000/sна 30s, построй гистограмму latency. -
k6 ramp test. Скрипт с ramp 0→500 VU за 2 минуты, thresholds p99<300ms; запусти, посмотри отчёт.
-
wrk2 vs ab. Сравни latency p99 для одного endpoint в wrk2 (open-loop) и ab (closed-loop) — должны различаться сильно при загруженном сервисе.
-
toxiproxy в тестах. В integration-suite подними toxiproxy перед Postgres, добавь 500ms latency, проверь что app срабатывает по timeout.
-
Chaos Mesh game day. Подними K8s локально (kind/minikube), задеплой app, примени
PodChaos(kill-pod), проверь что service остаётся доступным. -
Steady state hypothesis. Сформулируй гипотезу для своего сервиса; реализуй проверку через метрики Prometheus.
-
Soak test. Запусти vegeta
100/sв течение 6 часов; собери pprof в начале и в конце; сравни heap (memory leak?).
7. Дополнительные блоки
Заголовок раздела «7. Дополнительные блоки»7.1 Полный пример: parser + property + fuzz
Заголовок раздела «7.1 Полный пример: parser + property + fuzz»package csv
// Parse + Marshal — round trip propertyfunc TestParser_RoundTrip(t *testing.T) { rapid.Check(t, func(t *rapid.T) { rows := rapid.SliceOf( rapid.SliceOfN( rapid.StringMatching(`[^\n"]{0,40}`), 1, 6, ), ).Draw(t, "rows")
var buf bytes.Buffer require.NoError(t, Marshal(&buf, rows))
parsed, err := Parse(&buf) require.NoError(t, err) require.Equal(t, rows, parsed) })}
// Fuzz: ловим панику и не-recovery ошибкиfunc FuzzParser_NoCrash(f *testing.F) { f.Add([]byte("a,b,c\n1,2,3\n")) f.Add([]byte(`"quoted, value","escaped""quote"`)) f.Fuzz(func(t *testing.T, data []byte) { defer func() { if r := recover(); r != nil { t.Fatalf("parser panicked: %v", r) } }() _, _ = Parse(bytes.NewReader(data)) })}В норме fuzz запускают в CI на 1-10 минут; найденные corpus сохраняются в testdata/fuzz/FuzzParser_NoCrash/<hash>.
7.2 Метаморфические property-тесты
Заголовок раздела «7.2 Метаморфические property-тесты»Когда нет «эталонной» реализации, ищи отношения между разными inputs:
// log(a*b) == log(a) + log(b)rapid.Check(t, func(t *rapid.T) { a := rapid.Float64Range(1, 1e6).Draw(t, "a") b := rapid.Float64Range(1, 1e6).Draw(t, "b") lhs := math.Log(a * b) rhs := math.Log(a) + math.Log(b) require.InDelta(t, lhs, rhs, 1e-9)})
// сортировка инвариант: после удаления одного элемента — всё ещё отсортированоrapid.Check(t, func(t *rapid.T) { xs := rapid.SliceOfN(rapid.Int(), 1, 100).Draw(t, "xs") sort.Ints(xs) i := rapid.IntRange(0, len(xs)-1).Draw(t, "i") ys := append([]int(nil), xs[:i]...) ys = append(ys, xs[i+1:]...) require.True(t, sort.IntsAreSorted(ys))})7.3 gremlins: конфигурация
Заголовок раздела «7.3 gremlins: конфигурация».gremlins.yaml:
silent: falsethreshold: efficacy: 80 mutants-coverage: 70mutants: arithmetic-base: { enabled: true } conditionals-boundary: { enabled: true } conditionals-negation: { enabled: true } increment-decrement: { enabled: true } invert-negatives: { enabled: true }test-cpu: 4include: - ./internal/...exclude: - "_test.go" - "**/gen/**"gremlins run --tags=integration7.4 Распознавание шумов в mutation testing
Заголовок раздела «7.4 Распознавание шумов в mutation testing»- Survived в trivial-getter:
return xмутируется наreturn zero→ выживает, потому что getter обычно не тестируют отдельно. Это не «плохой тест», это естественно. - Equivalent mutants — например
i < len(arr)→i <= len(arr)-1в loop — эквивалентно. Помечай вручную как «excluded».
7.5 vegeta + Prometheus
Заголовок раздела «7.5 vegeta + Prometheus»echo "GET http://app/orders" | vegeta attack -rate=100/s -duration=2m | tee out.binvegeta report < out.binvegeta report -type=json < out.bin > metrics.jsonИмпортировать в Prometheus можно через vegeta-prometheus exporter или вручную распарсить JSON.
7.6 k6: распределённый load
Заголовок раздела «7.6 k6: распределённый load»k6 cloud (managed) или self-hosted k6-operator (Kubernetes):
apiVersion: k6.io/v1alpha1kind: TestRunmetadata: name: orders-loadspec: parallelism: 4 script: configMap: name: k6-test-script arguments: --out json=/results/result.json4 пода × N VU = распределённая нагрузка.
7.7 Гистограмма vs среднее: почему важно
Заголовок раздела «7.7 Гистограмма vs среднее: почему важно»Среднее latency = 50ms — звучит нормально.
Гистограмма: 0-10ms: ████████████████████ 80% 10-100ms: ███ 15% 1s-5s: █ 5% ← хвост!
p50 = 5ms, p99 = 4s.Среднее всегда обманчиво — используй гистограмму + p95/p99/p99.9.
7.8 Chaos Mesh: PodChaos и IOChaos
Заголовок раздела «7.8 Chaos Mesh: PodChaos и IOChaos»Kill pod:
apiVersion: chaos-mesh.org/v1alpha1kind: PodChaosspec: action: pod-kill mode: one selector: { labelSelectors: {"app": "orders"} } duration: "30s"Slow disk:
apiVersion: chaos-mesh.org/v1alpha1kind: IOChaosspec: action: latency mode: one selector: { labelSelectors: {"app": "orders"} } volumePath: /data path: /data/**/* delay: 100ms percent: 50 duration: "5m"Network partition:
apiVersion: chaos-mesh.org/v1alpha1kind: NetworkChaosspec: action: partition direction: both selector: { labelSelectors: {"app": "db"} } target: selector: { labelSelectors: {"app": "orders"} } duration: "1m"7.9 Chaos engineering: maturity model
Заголовок раздела «7.9 Chaos engineering: maturity model»| Уровень | Состояние |
|---|---|
| 1 | Никогда не пробовали |
| 2 | Эксперименты в staging вручную |
| 3 | Автоматизированные эксперименты в staging |
| 4 | Game days раз в квартал в production |
| 5 | Continuous chaos в production (Netflix) |
Не торопиться — сначала observability, потом resilience patterns, потом chaos.
7.10 Готовые runbook’и для game days
Заголовок раздела «7.10 Готовые runbook’и для game days»Минимальный шаблон:
- Гипотеза: «При kill 1 pod из 3 — клиенты видят < 10 ошибок 5xx за 2 минуты».
- Метод:
chaosctl pod-kill --label app=orders --count 1. - Мониторинг: Grafana дашборды, alert-канал.
- Abort criteria: error rate > 5%, p99 > 5s.
- Recovery: автоматическое восстановление K8s.
- Postmortem: что увидели, что фиксили.
7.11 Подходящие property-target в реальном коде
Заголовок раздела «7.11 Подходящие property-target в реальном коде»- Парсеры (JSON, CSV, protobuf, datetime).
- Сериализаторы (Marshal/Unmarshal round-trip).
- Шифрование (encrypt→decrypt).
- Compression (compress→decompress).
- Бизнес-инварианты (баланс счёта не отрицательный, сумма строк = total).
- Конкурентные структуры (lock-free queue корректен под N горутинами).
- Маршрутизация (для любого URL — попадает в нужный handler).
7.12 Чек-лист property + mutation + load + chaos
Заголовок раздела «7.12 Чек-лист property + mutation + load + chaos»- Property-тесты для парсеров, сериализаторов, бизнес-инвариантов.
- Fuzz target для каждого парсера / входной точки
[]byte. - Mutation testing раз в неделю в CI, baseline >= 80%.
- Load test: vegeta/k6 для критичных endpoint, p99 SLO зафиксирован.
- Soak test раз в спринт (memory leak hunt).
- toxiproxy в integration suite для критичных deps.
- Game day каждый квартал (один pod kill + один dep down).
- Steady state hypothesis для каждого эксперимента.
- Blast radius: phased rollout chaos.
- Observability в любом эксперименте.
7.13 Пример комплексного pipeline
Заголовок раздела «7.13 Пример комплексного pipeline» PR → CI ├─ unit tests (go test) ├─ integration tests (testcontainers, тэг) ├─ contract tests (Pact) ├─ property tests (rapid) ├─ fuzz 60s (go test -fuzz) ├─ mutation testing (раз в день, не на каждый PR) ├─ load smoke (k6, 30s ramp) merge → staging ├─ deploy ├─ chaos light (kill 1 pod) └─ verify SLO release → production ├─ canary ├─ progressive rollout └─ chaos game day quarterly8. Источники
Заголовок раздела «8. Источники»- pgregory.net/rapid — property-based testing.
- gopter — альтернатива.
- Go fuzzing tutorial — официально.
- go-gremlins/gremlins — mutation testing для Go.
- «PIT mutation testing — overview» — концептуально (Java-tool, но идеи универсальны).
- vegeta — CLI load tester.
- k6 documentation — modern load testing.
- wrk2 (constant throughput, no coordinated omission) — Gil Tene.
- «Coordinated Omission» talk by Gil Tene — must-watch для load.
- Chaos Engineering Principles — каноническое определение.
- Chaos Mesh docs — CNCF chaos для K8s.
- «Chaos Engineering» — O’Reilly book (Rosenthal, Jones) — глубокая книга.
- Netflix Tech Blog: Simian Army — историческая база.
- Shopify/toxiproxy — TCP-chaos для тестов.