Табличные тесты через пакет testing и go test
Содержание курса
Проверка валидатора пин-кода
Банковские приложения и системы электронных замков повсеместно используют короткие числовые коды для быстрой аутентификации. Чтобы такой код был пригоден к использованию, он должен удовлетворять минимальным требованиям безопасности: состоять ровно из четырёх цифр и не сводиться к четырём полностью одинаковым символам — такие комбинации, как «0000» или «9999», слишком очевидны и легко угадываются перебором.
Для этой цели уже написана функция-валидатор, которая принимает строку и сообщает, является ли та допустимым пин-кодом. Однако доверять подобной функции можно лишь тогда, когда она явно проверена на разнообразных входных данных: корректном коде, пустой строке, строке с буквами, значениях неверной длины и, конечно, последовательности из четырёх одинаковых цифр.
Здесь в дело вступает табличный тест: набор именованных случаев, каждый из которых описывает конкретную ситуацию и ожидаемый исход. Такой подход позволяет одним взглядом охватить весь диапазон поведения валидатора и легко добавить новые сценарии в будущем. Результатом работы должна стать дополненная таблица, в которой каждый случай запускается как самостоятельный именованный подтест, а при любом расхождении между фактическим и ожидаемым значением выводятся оба — чтобы причину сбоя можно было установить немедленно.
Что нужно сделать
Дополните validator_test.go. Создайте таблицу с именем, входным пин-кодом и ожидаемым bool; покройте корректный код, пустую строку, букву, короткую и длинную строки, четыре одинаковые цифры. Каждый случай запустите через t.Run, а несовпадение сообщайте через t.Errorf, показывая фактическое и ожидаемое значения. Реализацию ValidPIN менять нельзя.
О данных в ответах
Используйте учебные данные. Не вставляйте пароли, токены, ключи доступа, паспортные и банковские данные, а также персональные данные других людей. Политика обработки данных.
- Public go test ./...
- healthGET /health → 200
Сначала выполняется 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)
}
}
Отправьте решение, чтобы увидеть результаты тестов.
