TDD в Go: пишем код без страха и упрека

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Вступление: почему TDD — это не религия, а спасательный круг

Представьте, что вы чините проводку в старом доме. Вы выключаете рубильник, но всё равно боитесь сунуть руки в щиток. Примерно так чувствует себя разработчик, который вносит изменения в проект без тестов. Особенно в Go — языке, где статическая типизация даёт ложное чувство безопасности.

Разработка через тесты (TDD) в Go — это не модный тренд и не сектантский ритуал. Это методология, которая превращает страх перед рефакторингом в уверенность. Согласно исследованию IBM, проекты с TDD показывают на 40% меньше дефектов. А в Go, где компилятор уже отсекает половину ошибок, TDD добивает оставшиеся 50%.

В этой статье мы не будем читать лекции. Мы возьмём реальную задачу: напишем небольшой HTTP-сервис для сокращения ссылок. И пройдём весь путь TDD — от красного теста до зелёного и рефакторинга. Поехали!

Что такое TDD на самом деле?

Если вы слышали про Red-Green-Refactor — вы уже в теме. Если нет — запомните три простых шага:

  1. Red — пишем тест, который заведомо падает (потому что функционала ещё нет).
  2. Green — пишем минимальный код, чтобы тест прошёл. Не идеальный, а просто работающий.
  3. 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                

Блоги

Рекомендаций по книгам