Вступление: почему TDD — это не религия, а спасательный круг
Представьте, что вы чините проводку в старом доме. Вы выключаете рубильник, но всё равно боитесь сунуть руки в щиток. Примерно так чувствует себя разработчик, который вносит изменения в проект без тестов. Особенно в Go — языке, где статическая типизация даёт ложное чувство безопасности.
Разработка через тесты (TDD) в Go — это не модный тренд и не сектантский ритуал. Это методология, которая превращает страх перед рефакторингом в уверенность. Согласно исследованию IBM, проекты с TDD показывают на 40% меньше дефектов. А в Go, где компилятор уже отсекает половину ошибок, TDD добивает оставшиеся 50%.
В этой статье мы не будем читать лекции. Мы возьмём реальную задачу: напишем небольшой HTTP-сервис для сокращения ссылок. И пройдём весь путь TDD — от красного теста до зелёного и рефакторинга. Поехали!
Что такое TDD на самом деле?
Если вы слышали про Red-Green-Refactor — вы уже в теме. Если нет — запомните три простых шага:
- Red — пишем тест, который заведомо падает (потому что функционала ещё нет).
- Green — пишем минимальный код, чтобы тест прошёл. Не идеальный, а просто работающий.
- Refactor — улучшаем код, не меняя его поведение. Тесты — наша страховка.
В Go этот цикл особенно приятен благодаря встроенному пакету testing. Вам не нужно ставить jest или pytest — всё уже есть в стандартной библиотеке. Но многие новички игнорируют тесты, считая, что Go и так «безопасный». И зря.
Почему TDD в Go работает лучше, чем в других языках?
Go-сообщество обожает простоту. TDD здесь не превращается в бюрократию с моками и стабами. Вместо этого вы пишете табличные тесты (table-driven tests) — компактные, читаемые, понятные. Один тест-кейс — одна строчка. И да, Go-тесты работают быстро. Миллисекунды на сотни кейсов — норма.
Кстати, в официальном блоге Go не раз подчёркивали: «Тесты — это документация». Если ваш тест читается как спецификация — вы всё делаете правильно.
Настраиваем окружение: что нужно для старта
Предполагаю, что Go у вас уже установлен. Если нет — brew install go или apt install golang-go. Проверьте версию: go version. Для статьи хватит Go 1.21+.
Создадим проект:
mkdir url-shortener-tdd
cd url-shortener-tdd
go mod init url-shortener
Готово. Никаких фреймворков — только стандартная библиотека и разработка через тесты Go.
Пишем первый тест: Red
Допустим, мы хотим функцию Shorten(url string) string, которая превращает длинный URL в короткий хеш. Пока её нет — и это отлично. Напишем тест:
// shortener_test.go
package main
import "testing"
func TestShorten(t *testing.T) {
url := "https://pythonlib.ru/very/long/path?with=params&and=stuff"
result := Shorten(url)
if len(result) == 0 {
t.Error("Shorten вернул пустую строку")
}
if len(result) > 10 {
t.Errorf("Ожидал короткий хеш, получил %d символов", len(result))
}
}
Запускаем: go test -v. Видим красный (ну, технически — ошибку компиляции). Функции Shorten нет. Отлично, мы на этапе Red.
Минимальный код: Green
Теперь пишем ровно столько, чтобы тест прошёл. Никакой криптографии, просто хеш:
// shortener.go
package main
import (
"crypto/sha256"
"fmt"
)
func Shorten(url string) string {
hash := sha256.Sum256([]byte(url))
return fmt.Sprintf("%x", hash[:4]) // берём первые 4 байта
}
Запускаем тест снова. Зелёный? Прекрасно. Но код ужасен — хеш коллизионен, и мы даже не проверяем, что URL валидный. Но TDD не просит идеала на первом шаге. Он просит рабочий код.
Рефакторинг: делаем код приличным
Теперь, когда тест защищает нас, можно улучшать. Добавим проверку URL и используем base64 для компактности:
package main
import (
"crypto/sha256"
"encoding/base64"
"net/url"
)
func Shorten(rawURL string) (string, error) {
_, err := url.ParseRequestURI(rawURL)
if err != nil {
return "", fmt.Errorf("invalid URL: %w", err)
}
hash := sha256.Sum256([]byte(rawURL))
short := base64.URLEncoding.EncodeToString(hash[:6])
return short[:8], nil
}
Тест придётся обновить — теперь функция возвращает ошибку. Но TDD учит нас: сначала меняем тест, потом код. Обновлённый тест:
func TestShorten(t *testing.T) {
url := "https://pythonlib.ru/very/long/path"
result, err := Shorten(url)
if err != nil {
t.Fatalf("Не ожидали ошибку: %v", err)
}
if len(result) != 8 {
t.Errorf("Ожидали 8 символов, получили %d", len(result))
}
}
Запускаем — зелёный. Мы только что прошли полный цикл TDD в Go.
HTTP-сервер с TDD: от theory до practice
Теперь усложним задачу. Нам нужен HTTP-сервер с двумя эндпоинтами:
POST /shorten— принимает JSON с URL, возвращает короткую ссылкуGET /{short}— редиректит на оригинальный URL
В TDD мы начинаем с теста для HTTP-хендлера. В Go это делается через httptest.NewRecorder:
// handler_test.go
package main
import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func TestShortenHandler(t *testing.T) {
body := `{"url": "https://pythonlib.ru"}`
req := httptest.NewRequest("POST", "/shorten", strings.NewReader(body))
w := httptest.NewRecorder()
// Пока хендлера нет, мы его напишем
ShortenHandler(w, req)
if w.Code != http.StatusOK {
t.Errorf("Ожидали 200, получили %d", w.Code)
}
var resp map[string]string
json.NewDecoder(w.Body).Decode(&resp)
if _, ok