Пакеты, тестирование, файлы и JSON

Табличные тесты через пакет testing и go test

Содержание курса

Проверка валидатора пин-кода

Банковские приложения и системы электронных замков повсеместно используют короткие числовые коды для быстрой аутентификации. Чтобы такой код был пригоден к использованию, он должен удовлетворять минимальным требованиям безопасности: состоять ровно из четырёх цифр и не сводиться к четырём полностью одинаковым символам — такие комбинации, как «0000» или «9999», слишком очевидны и легко угадываются перебором.

Для этой цели уже написана функция-валидатор, которая принимает строку и сообщает, является ли та допустимым пин-кодом. Однако доверять подобной функции можно лишь тогда, когда она явно проверена на разнообразных входных данных: корректном коде, пустой строке, строке с буквами, значениях неверной длины и, конечно, последовательности из четырёх одинаковых цифр.

Здесь в дело вступает табличный тест: набор именованных случаев, каждый из которых описывает конкретную ситуацию и ожидаемый исход. Такой подход позволяет одним взглядом охватить весь диапазон поведения валидатора и легко добавить новые сценарии в будущем. Результатом работы должна стать дополненная таблица, в которой каждый случай запускается как самостоятельный именованный подтест, а при любом расхождении между фактическим и ожидаемым значением выводятся оба — чтобы причину сбоя можно было установить немедленно.

Что нужно сделать

Дополните validator_test.go. Создайте таблицу с именем, входным пин-кодом и ожидаемым bool; покройте корректный код, пустую строку, букву, короткую и длинную строки, четыре одинаковые цифры. Каждый случай запустите через t.Run, а несовпадение сообщайте через t.Errorf, показывая фактическое и ожидаемое значения. Реализацию ValidPIN менять нельзя.

О данных в ответах

Используйте учебные данные. Не вставляйте пароли, токены, ключи доступа, паспортные и банковские данные, а также персональные данные других людей. Политика обработки данных.

Проверяется тестами (2)
  • Public go test ./...
  • health
    GET /health200
Go-проект · validator_test.go

Сначала выполняется go test ./..., затем сервис собирается и проверяется HTTP-запросами. Внешняя сеть и скачивание модулей отключены.

validator.go
package main

func ValidPIN(pin string) bool {
    if len(pin) != 4 { return false }
    for i := 0; i < len(pin); i++ { if pin[i] < '0' || pin[i] > '9' { return false } }
    allSame := true
    for i := 1; i < len(pin); i++ { if pin[i] != pin[0] { allSame = false } }
    return !allSame
}
meta_test.go
package main

import (
    "context"
    "os"
    "os/exec"
    "path/filepath"
    "runtime"
    "strings"
    "testing"
    "time"
)

func runPINStudentTests(t *testing.T, validatorSource string) (bool, string) {
    t.Helper()
    dir := t.TempDir()
    student, err := os.ReadFile("validator_test.go"); if err != nil { t.Fatal(err) }
    if err := os.WriteFile(filepath.Join(dir, "validator_test.go"), student, 0o600); err != nil { t.Fatal(err) }
    if err := os.WriteFile(filepath.Join(dir, "validator.go"), []byte(validatorSource), 0o600); err != nil { t.Fatal(err) }
    if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module mutation.local/pin\n\ngo 1.27\n"), 0o600); err != nil { t.Fatal(err) }
    ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second); defer cancel()
    binary := filepath.Join(dir, "pin-tests"); if runtime.GOOS == "windows" { binary += ".exe" }
    compile := exec.CommandContext(ctx, "go", "test", "-c", "-o", binary); compile.Dir = dir; compile.Env = append(os.Environ(), "GOWORK=off")
    if output, err := compile.CombinedOutput(); err != nil { return false, string(output) }
    for _, name := range []string{"validator.go", "validator_test.go"} { if err := os.Remove(filepath.Join(dir, name)); err != nil { t.Fatal(err) } }
    run := exec.CommandContext(ctx, binary, "-test.count=1"); run.Dir = dir; output, err := run.CombinedOutput()
    if ctx.Err() != nil { return false, "student suite timed out" }
    return err == nil, string(output)
}

func TestStudentSuiteKillsPINMutations(t *testing.T) {
    data, err := os.ReadFile("validator.go"); if err != nil { t.Fatal(err) }; correct := string(data)
    if ok, output := runPINStudentTests(t, correct); !ok { t.Fatalf("tests fail on correct validator:\n%s", output) }
    mutations := []struct{name, old, new string}{
        {"valid rejected", "return !allSame", "return false"},
        {"wrong length accepted", "if len(pin) != 4", "if len(pin) > 4"},
        {"letters accepted", "if pin[i] < '0' || pin[i] > '9' { return false }", "if false { return false }"},
        {"repeated accepted", "return !allSame", "return true"},
    }
    for _, m := range mutations { t.Run(m.name, func(t *testing.T) {
        changed := strings.Replace(correct, m.old, m.new, 1); if changed == correct { t.Fatalf("mutation target disappeared: %s", m.old) }
        if ok, output := runPINStudentTests(t, changed); ok { t.Fatalf("student tests did not kill %s\n%s", m.name, output) }
    }) }
}
main.go
package main

import (
    "io"
    "log"
    "net/http"
)

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "text/plain; charset=utf-8")
        w.WriteHeader(http.StatusOK)
        _, _ = io.WriteString(w, "ok\n")
    })
    if err := http.ListenAndServe(":18080", mux); err != nil {
        log.Fatal(err)
    }
}
РешениеGo · Go 1.27 (Modal)

Отправьте решение, чтобы увидеть результаты тестов.