> For the complete documentation index, see [llms.txt](https://eda-1.gitbook.io/lgwt/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://eda-1.gitbook.io/lgwt/osnovy-go/roman-numerals.md).

# Введение в тестирование на основе свойств

[**Весь код для этой главы вы найдете здесь**](https://github.com/quii/learn-go-with-tests/tree/main/roman-numerals)

Некоторые компании могут попросить вас выполнить [ката "Римские цифры"](http://codingdojo.org/kata/RomanNumerals/) в рамках собеседования. Эта глава покажет, как вы можете подойти к ней с использованием TDD.

Мы собираемся написать функцию, которая преобразует [арабское число](https://en.wikipedia.org/wiki/Arabic_numerals) (цифры от 0 до 9) в римскую цифру.

Если вы не слышали о [римских цифрах](https://en.wikipedia.org/wiki/Roman_numerals), то это способ, которым римляне записывали числа.

Вы строите их, соединяя символы, и эти символы представляют числа

Так `I` — это "один". `III` — это три.

Кажется легко, но есть несколько интересных правил. `V` означает пять, но `IV` — это 4 (а не `IIII`).

`MCMLXXXIV` — это 1984. Это выглядит сложно, и трудно представить, как мы можем написать код, чтобы понять это с самого начала.

Как подчеркивает эта книга, ключевым навыком для разработчиков программного обеспечения является попытка определить "тонкие вертикальные срезы" *полезной* функциональности, а затем **итерация**. Рабочий процесс TDD помогает облегчить итеративную разработку.

Поэтому вместо 1984, давайте начнем с 1.

## Сначала пишем тест

```go
func TestRomanNumerals(t *testing.T) {
	got := ConvertToRoman(1)
	want := "I"

	if got != want {
		t.Errorf("got %q, want %q", got, want)
	}
}
```

Если вы зашли так далеко в книге, то это, надеюсь, кажется вам очень скучным и обыденным. Это хорошо.

## Попробуем запустить тест

```console
./numeral_test.go:6:9: undefined: ConvertToRoman
```

Позвольте компилятору указать путь

## Пишем минимальный код, чтобы тест запустился, и проверяем вывод упавшего теста

Создаем нашу функцию, но пока не заставляем тест проходить, всегда убеждайтесь, что тесты падают так, как вы ожидаете.

```go
func ConvertToRoman(arabic int) string {
	return ""
}
```

Теперь он должен запуститься

```console
=== RUN   TestRomanNumerals
--- FAIL: TestRomanNumerals (0.00s)
    numeral_test.go:10: got '', want 'I'
FAIL
```

## Пишем достаточно кода, чтобы тест прошел

```go
func ConvertToRoman(arabic int) string {
	return "I"
}
```

## Рефакторинг

Пока особо нечего рефакторить.

*Я знаю*, это кажется странным просто жестко кодировать результат, но с TDD мы хотим как можно дольше оставаться вне "красной зоны". Возможно, *кажется*, что мы не добились многого, но мы определили наш API и получили тест, фиксирующий одно из наших правил; даже если "настоящий" код довольно глуп.

Теперь используйте это неприятное чувство, чтобы написать новый тест, который заставит нас написать чуть менее глупый код.

## Сначала пишем тест

Мы можем использовать подтесты, чтобы аккуратно сгруппировать наши тесты.

```go
func TestRomanNumerals(t *testing.T) {
	t.Run("1 gets converted to I", func(t *testing.T) {
		got := ConvertToRoman(1)
		want := "I"

		if got != want {
			t.Errorf("got %q, want %q", got, want)
		}
	})

	t.Run("2 gets converted to II", func(t *testing.T) {
		got := ConvertToRoman(2)
		want := "II"

		if got != want {
			t.Errorf("got %q, want %q", got, want)
		}
	})
}
```

## Попробуем запустить тест

```console
=== RUN   TestRomanNumerals/2_gets_converted_to_II
    --- FAIL: TestRomanNumerals/2_gets_converted_to_II (0.00s)
        numeral_test.go:20: got 'I', want 'II'
```

Здесь нет ничего удивительного.

## Пишем достаточно кода, чтобы тест прошел

```go
func ConvertToRoman(arabic int) string {
	if arabic == 2 {
		return "II"
	}
	return "I"
}
```

Да, всё ещё кажется, что мы не решаем проблему на самом деле. Поэтому нам нужно написать больше тестов, чтобы двигаться вперед.

## Рефакторинг

У нас есть некоторое повторение в наших тестах. Когда вы тестируете что-то, что кажется вопросом "для заданного ввода X мы ожидаем Y", вам, вероятно, следует использовать табличные тесты.

```go
func TestRomanNumerals(t *testing.T) {
	cases := []struct {
		Description string
		Arabic      int
		Want        string
	}{
		{"1 gets converted to I", 1, "I"},
		{"2 gets converted to II", 2, "II"},
	}

	for _, test := range cases {
		t.Run(test.Description, func(t *testing.T) {
			got := ConvertToRoman(test.Arabic)
			if got != test.Want {
				t.Errorf("got %q, want %q", got, test.Want)
			}
		})
	}
}
```

Теперь мы можем легко добавлять больше кейсов, не написав при этом больше шаблонного кода для тестов.

Давайте продолжим и перейдем к 3

## Сначала пишем тест

Добавьте следующее в наши кейсы

```
{"3 gets converted to III", 3, "III"},
```

## Попробуем запустить тест

```console
=== RUN   TestRomanNumerals/3_gets_converted_to_III
    --- FAIL: TestRomanNumerals/3_gets_converted_to_III (0.00s)
        numeral_test.go:20: got 'I', want 'III'
```

## Пишем достаточно кода, чтобы тест прошел

```go
func ConvertToRoman(arabic int) string {
	if arabic == 3 {
		return "III"
	}
	if arabic == 2 {
		return "II"
	}
	return "I"
}
```

## Рефакторинг

Хорошо, мне начинают не нравиться эти операторы if, и если вы достаточно внимательно посмотрите на код, вы увидите, что мы строим строку из `I` в зависимости от размера `arabic`.

Мы "знаем", что для более сложных чисел мы будем выполнять какие-то арифметические операции и конкатенацию строк.

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

```go
func ConvertToRoman(arabic int) string {

	var result strings.Builder

	for i := 0; i < arabic; i++ {
		result.WriteString("I")
	}

	return result.String()
}
```

Вы, возможно, помните [`strings.Builder`](https://golang.org/pkg/strings/#Builder) из нашего обсуждения [бенчмаркинга](/lgwt/osnovy-go/iteration.md#benchmarking)

> Builder используется для эффективного построения строки с помощью методов Write. Он минимизирует копирование памяти.

Обычно я бы не заморачивался с такими оптимизациями, пока не столкнулся бы с реальной проблемой производительности, но объем кода не намного больше, чем "ручное" добавление к строке, так что мы можем использовать более быстрый подход.

Код выглядит для меня лучше и описывает домен *настолько, насколько мы его знаем сейчас*.

### Римляне тоже любили DRY...

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

Вместо этого вы берете следующий по величине символ, а затем "вычитаете", помещая символ слева от него. Не все символы могут использоваться как вычитающие; только I (1), X (10) и C (100).

Например, `5` в римских цифрах — это `V`. Чтобы получить 4, вы не пишете `IIII`, а пишете `IV`.

Вычитающий символ может быть расположен только перед одним из двух следующих за ним более высоких символов в его «семействе»: `I` может предшествовать только `V` или `X` (4 — это `IV`, 9 — это `IX`), `X` может предшествовать только `L` или `C` (40 — это `XL`, 90 — это `XC`), а `C` может предшествовать только `D` или `M` (400 — это `CD`, 900 — это `CM`). Таким образом, хотя `I`, `X` и `C` являются допустимыми вычитающими символами, вы не можете свободно их смешивать и сочетать — 99 — это не `IC` (это `XCIX`, 90 + 9), а 499 — это не `ID` (это `CDXCIX`, 400 + 90 + 9).

## Сначала пишем тест

```
{"4 gets converted to IV (can't repeat more than 3 times)", 4, "IV"},
```

## Попробуем запустить тест

```console
=== RUN   TestRomanNumerals/4_gets_converted_to_IV_(cant_repeat_more_than_3_times)
    --- FAIL: TestRomanNumerals/4_gets_converted_to_IV_(cant_repeat_more_than_3_times) (0.00s)
        numeral_test.go:24: got 'IIII', want 'IV'
```

## Пишем достаточно кода, чтобы тест прошел

```go
func ConvertToRoman(arabic int) string {

	if arabic == 4 {
		return "IV"
	}

	var result strings.Builder

	for i := 0; i < arabic; i++ {
		result.WriteString("I")
	}

	return result.String()
}
```

## Рефакторинг

Мне не "нравится", что мы нарушили наш шаблон построения строк, и я хочу продолжать его использовать.

```go
func ConvertToRoman(arabic int) string {

	var result strings.Builder

	for i := arabic; i > 0; i-- {
		if i == 4 {
			result.WriteString("IV")
			break
		}
		result.WriteString("I")
	}

	return result.String()
}
```

Чтобы 4 "вписалось" в мое текущее мышление, я теперь отсчитываю в обратном порядке от арабского числа, добавляя символы к нашей строке по мере продвижения. Не уверен, что это сработает в долгосрочной перспективе, но давайте посмотрим!

Давайте заставим работать 5

## Сначала пишем тест

```
{"5 gets converted to V", 5, "V"},
```

## Попробуем запустить тест

```console
=== RUN   TestRomanNumerals/5_gets_converted_to_V
    --- FAIL: TestRomanNumerals/5_gets_converted_to_V (0.00s)
        numeral_test.go:25: got 'IIV', want 'V'
```

## Пишем достаточно кода, чтобы тест прошел

Просто скопируйте подход, который мы использовали для 4

```go
func ConvertToRoman(arabic int) string {

	var result strings.Builder

	for i := arabic; i > 0; i-- {
		if i == 5 {
			result.WriteString("V")
			break
		}
		if i == 4 {
			result.WriteString("IV")
			break
		}
		result.WriteString("I")
	}

	return result.String()
}
```

## Рефакторинг

Повторение в таких циклах обычно является признаком того, что абстракция ждет своего часа. Прерывание циклов может быть эффективным инструментом для читаемости, но также может говорить о чем-то другом.

Мы перебираем наше арабское число, и если мы встречаем определенные символы, мы вызываем `break`, но на самом деле мы *неуклюже* вычитаем из `i`.

```go
func ConvertToRoman(arabic int) string {

	var result strings.Builder

	for arabic > 0 {
		switch {
		case arabic > 4:
			result.WriteString("V")
			arabic -= 5
		case arabic > 3:
			result.WriteString("IV")
			arabic -= 4
		default:
			result.WriteString("I")
			arabic--
		}
	}

	return result.String()
}
```

* Учитывая сигналы, которые я получаю из нашего кода, основанные на наших тестах очень базовых сценариев, я вижу, что для построения римской цифры мне нужно вычитать из `arabic` по мере применения символов.
* Цикл `for` больше не зависит от `i`, и вместо этого мы будем продолжать строить нашу строку, пока не вычтем достаточно символов из `arabic`.

Я почти уверен, что этот подход будет верен и для 6 (VI), 7 (VII), и 8 (VIII). Тем не менее, добавьте эти кейсы в наш набор тестов и проверьте (я не буду включать код для краткости, проверьте github для примеров, если не уверены).

9 следует тому же правилу, что и 4: мы должны вычитать `I` из представления следующего числа. 10 в римских цифрах обозначается как `X`; следовательно, 9 должно быть `IX`.

## Сначала пишем тест

```
{"9 gets converted to IX", 9, "IX"},
```

## Попробуем запустить тест

```console
=== RUN   TestRomanNumerals/9_gets_converted_to_IX
    --- FAIL: TestRomanNumerals/9_gets_converted_to_IX (0.00s)
        numeral_test.go:29: got 'VIV', want 'IX'
```

## Пишем достаточно кода, чтобы тест прошел

Мы должны быть в состоянии применить тот же подход, что и раньше.

```
case arabic > 8:
    result.WriteString("IX")
    arabic -= 9
```

## Рефакторинг

*Чувствуется*, что код всё ещё намекает на необходимость рефакторинга где-то, но это мне не совсем очевидно, поэтому давайте продолжим.

Я также пропущу код для этого, но добавьте в ваши тестовые кейсы тест для `10`, который должен быть `X`, и заставьте его пройти, прежде чем читать дальше.

Вот несколько тестов, которые я добавил, поскольку я уверен, что наш код должен работать до 39:

```
{"10 gets converted to X", 10, "X"},
{"14 gets converted to XIV", 14, "XIV"},
{"18 gets converted to XVIII", 18, "XVIII"},
{"20 gets converted to XX", 20, "XX"},
{"39 gets converted to XXXIX", 39, "XXXIX"},
```

Если вы когда-либо занимались ООП, то знаете, что к операторам `switch` следует относиться с долей подозрения. Обычно вы захватываете концепцию или данные внутри некоторого императивного кода, хотя на самом деле их можно было бы инкапсулировать в структуру класса.

Go не является строго ООП, но это не означает, что мы полностью игнорируем уроки, которые предлагает ООП (как бы некоторые ни хотели вам сказать).

Наш оператор switch описывает некоторые истины о римских цифрах наряду с поведением.

Мы можем провести рефакторинг, отделив данные от поведения.

```go
type RomanNumeral struct {
	Value  int
	Symbol string
}

var allRomanNumerals = []RomanNumeral{
	{10, "X"},
	{9, "IX"},
	{5, "V"},
	{4, "IV"},
	{1, "I"},
}

func ConvertToRoman(arabic int) string {

	var result strings.Builder

	for _, numeral := range allRomanNumerals {
		for arabic >= numeral.Value {
			result.WriteString(numeral.Symbol)
			arabic -= numeral.Value
		}
	}

	return result.String()
}
```

Это гораздо лучше. Мы объявили некоторые правила, касающиеся чисел, как данные, а не скрытые в алгоритме, и мы видим, как мы просто перебираем арабское число, пытаясь добавить символы к нашему результату, если они подходят.

Работает ли эта абстракция для больших чисел? Расширьте набор тестов, чтобы он работал для римской цифры 50, которая является `L`.

Вот несколько тестовых кейсов, попробуйте заставить их пройти.

```
{"40 gets converted to XL", 40, "XL"},
{"47 gets converted to XLVII", 47, "XLVII"},
{"49 gets converted to XLIX", 49, "XLIX"},
{"50 gets converted to L", 50, "L"},
```

Нужна помощь? Вы можете увидеть, какие символы добавить, в [этом gist'е](https://gist.github.com/pamelafox/6c7b948213ba55332d86efd0f0b037de).

## И остальные!

Вот остальные символы

| Arabic | Roman |
| ------ | :---: |
| 100    |   C   |
| 500    |   D   |
| 1000   |   M   |

Примените тот же подход для оставшихся символов, это должно быть просто вопросом добавления данных как в тесты, так и в наш срез символов.

Работает ли ваш код для `1984`: `MCMLXXXIV` ?

Вот мой окончательный набор тестов

```go
func TestRomanNumerals(t *testing.T) {
	cases := []struct {
		Arabic int
		Roman  string
	}{
		{Arabic: 1, Roman: "I"},
		{Arabic: 2, Roman: "II"},
		{Arabic: 3, Roman: "III"},
		{Arabic: 4, Roman: "IV"},
		{Arabic: 5, "V"},
		{Arabic: 6, Roman: "VI"},
		{Arabic: 7, Roman: "VII"},
		{Arabic: 8, Roman: "VIII"},
		{Arabic: 9, Roman: "IX"},
		{Arabic: 10, Roman: "X"},
		{Arabic: 14, Roman: "XIV"},
		{Arabic: 18, Roman: "XVIII"},
		{Arabic: 20, Roman: "XX"},
		{Arabic: 39, Roman: "XXXIX"},
		{Arabic: 40, Roman: "XL"},
		{Arabic: 47, Roman: "XLVII"},
		{Arabic: 49, Roman: "XLIX"},
		{Arabic: 50, Roman: "L"},
		{Arabic: 100, Roman: "C"},
		{Arabic: 90, Roman: "XC"},
		{Arabic: 400, Roman: "CD"},
		{Arabic: 500, Roman: "D"},
		{Arabic: 900, Roman: "CM"},
		{Arabic: 1000, Roman: "M"},
		{Arabic: 1984, Roman: "MCMLXXXIV"},
		{Arabic: 3999, Roman: "MMMCMXCIX"},
		{Arabic: 2014, Roman: "MMXIV"},
		{Arabic: 1006, Roman: "MVI"},
		{Arabic: 798, Roman: "DCCXCVIII"},
	}
	for _, test := range cases {
		t.Run(fmt.Sprintf("%d gets converted to %q", test.Arabic, test.Roman), func(t *testing.T) {
			got := ConvertToRoman(test.Arabic)
			if got != test.Roman {
				t.Errorf("got %q, want %q", got, test.Roman)
			}
		})
	}
}
```

* Я удалил `description`, так как посчитал, что *данные* достаточно описывают информацию.
* Я добавил несколько других граничных случаев, которые нашел, просто чтобы придать себе немного больше уверенности. С табличными тестами это очень дешево сделать.

Я не менял алгоритм, всё, что мне нужно было сделать, это обновить срез `allRomanNumerals`.

```go
var allRomanNumerals = []RomanNumeral{
	{1000, "M"},
	{900, "CM"},
	{500, "D"},
	{400, "CD"},
	{100, "C"},
	{90, "XC"},
	{50, "L"},
	{40, "XL"},
	{10, "X"},
	{9, "IX"},
	{5, "V"},
	{4, "IV"},
	{1, "I"},
}
```

## Разбор римских цифр

Мы еще не закончили. Далее мы напишем функцию, которая преобразует *из* римской цифры в `int`.

## Сначала пишем тест

Мы можем повторно использовать наши тестовые кейсы здесь с небольшим рефакторингом.

Переместите переменную `cases` за пределы теста как переменную пакета в блоке `var`.

```go
func TestConvertingToArabic(t *testing.T) {
	for _, test := range cases[:1] {
		t.Run(fmt.Sprintf("%q gets converted to %d", test.Roman, test.Arabic), func(t *testing.T) {
			got := ConvertToArabic(test.Roman)
			if got != test.Arabic {
				t.Errorf("got %d, want %d", got, test.Arabic)
			}
		})
	}
}
```

Обратите внимание, что я использую функциональность среза, чтобы пока что запустить только один из тестов (`cases[:1]`), так как попытка заставить все эти тесты пройти одновременно — слишком большой скачок.

## Попробуем запустить тест

```console
./numeral_test.go:60:11: undefined: ConvertToArabic
```

## Пишем минимальный код, чтобы тест запустился, и проверяем вывод упавшего теста

Добавляем определение нашей новой функции

```go
func ConvertToArabic(roman string) int {
	return 0
}
```

Теперь тест должен запуститься и упасть

```console
--- FAIL: TestConvertingToArabic (0.00s)
    --- FAIL: TestConvertingToArabic/'I'_gets_converted_to_1 (0.00s)
        numeral_test.go:62: got 0, want 1
```

## Пишем достаточно кода, чтобы тест прошел

Вы знаете, что делать

```go
func ConvertToArabic(roman string) int {
	return 1
}
```

Далее измените индекс среза в нашем тесте, чтобы перейти к следующему тестовому кейсу (например, `cases[:2]`). Заставьте его пройти с помощью самого глупого кода, который вы можете придумать, продолжайте писать глупый код (лучшая книга, правда?) и для третьего кейса. Вот мой глупый код.

```go
func ConvertToArabic(roman string) int {
	if roman == "III" {
		return 3
	}
	if roman == "II" {
		return 2
	}
	return 1
}
```

Через глупость *реального рабочего кода* мы можем начать видеть паттерн, как и раньше. Нам нужно пройтись по вводу и построить *что-то*, в данном случае общую сумму.

```go
func ConvertToArabic(roman string) int {
	total := 0
	for range roman {
		total++
	}
	return total
}
```

## Сначала пишем тест

Далее мы переходим к `cases[:4]` (`IV`), который теперь падает, потому что возвращает 2, так как это длина строки.

## Пишем достаточно кода, чтобы тест прошел

```go
// earlier..
var allRomanNumerals = RomanNumerals{
	{1000, "M"},
	{900, "CM"},
	{500, "D"},
	{400, "CD"},
	{100, "C"},
	{90, "XC"},
	{50, "L"},
	{40, "XL"},
	{10, "X"},
	{9, "IX"},
	{5, "V"},
	{4, "IV"},
	{1, "I"},
}

// later..
func ConvertToArabic(roman string) int {
	var arabic = 0

	for _, numeral := range allRomanNumerals {
		for strings.HasPrefix(roman, numeral.Symbol) {
			arabic += numeral.Value
			roman = strings.TrimPrefix(roman, numeral.Symbol)
		}
	}

	return arabic
}
```

Это, по сути, алгоритм `ConvertToRoman(int)`, реализованный в обратном порядке. Здесь мы перебираем заданную строку римских цифр:

* Мы ищем символы римских цифр из `allRomanNumerals`, от наибольшего к наименьшему, в начале строки.
* Если мы находим префикс, мы добавляем его значение к `arabic` и отрезаем префикс.

В конце мы возвращаем сумму как арабское число.

Функция `HasPrefix(s, prefix)` проверяет, начинается ли строка `s` с `prefix`, а `TrimPrefix(s, prefix)` удаляет `prefix` из `s`, поэтому мы можем продолжить работу с оставшимися символами римских цифр. Это работает с `IV` и всеми другими тестовыми кейсами.

Вы можете реализовать это как рекурсивную функцию, которая более элегантна (на мой взгляд), но может быть медленнее. Я оставляю это на ваше усмотрение и на некоторые тесты `Benchmark...`.

Теперь, когда у нас есть функции для преобразования арабского числа в римскую цифру и обратно, мы можем продвинуть наши тесты на шаг дальше:

## Введение в тестирование на основе свойств

В этой главе мы работали с несколькими правилами в области римских цифр:

* Не может быть более 3 последовательных символов
* Только I (1), X (10) и C (100) могут быть "вычитающими"
* Взятие результата `ConvertToRoman(N)` и передача его в `ConvertToArabic` должно вернуть нам `N`

Написанные нами тесты до сих пор можно описать как "примерные" тесты, где мы предоставляем *примеры* для проверки инструментам.

Что если бы мы могли взять эти правила, которые мы знаем о нашем домене, и каким-то образом применить их к нашему коду?

Тесты на основе свойств помогают вам в этом, подбрасывая случайные данные в ваш код и проверяя, что описанные вами правила всегда верны. Многие люди думают, что тесты на основе свойств в основном связаны со случайными данными, но они ошибаются. Настоящая сложность тестов на основе свойств заключается в *хорошем* понимании вашего домена, чтобы вы могли написать эти свойства.

Хватит слов, давайте посмотрим на код

> **⚠️ Пользователи Linux:** Пожалуйста, **НЕ** запускайте тест ниже немедленно. Это, вероятно, приведет к зависанию вашей системы (потребуется жесткая перезагрузка).
>
> <details>
>
> <summary>Нажмите здесь, чтобы узнать почему (Техническое объяснение)</summary>
>
> Пакет `testing/quick` генерирует случайные целые числа до максимального значения `int64`. Наша текущая наивная реализация пытается построить строку такой длины в памяти (квадриллионы символов).
>
> В то время как macOS и Windows часто справляются с этим изящно (интерфейс остается отзывчивым), ядра Linux обычно сталкиваются с "swap thrashing", что приводит к зависанию всей системы до того, как процесс может быть завершен.
>
> </details>

```go
func TestPropertiesOfConversion(t *testing.T) {
	assertion := func(arabic int) bool {
		roman := ConvertToRoman(arabic)
		fromRoman := ConvertToArabic(roman)
		return fromRoman == arabic
	}

	if err := quick.Check(assertion, nil); err != nil {
		t.Error("failed checks", err)
	}
}
```

### Обоснование свойства

Наш первый тест проверит, что если мы преобразуем число в римское, а затем используем другую нашу функцию для обратного преобразования в число, то мы получим исходное значение.

* Дано случайное число (например, `4`).
* Вызываем `ConvertToRoman` со случайным числом (должно вернуть `IV`, если `4`).
* Берем результат выше и передаем его в `ConvertToArabic`.
* Вышеизложенное должно дать нам наш исходный ввод (`4`).

Это кажется хорошим тестом для повышения нашей уверенности, потому что он должен сломаться, если в одном из них есть ошибка. Единственный способ, которым он мог бы пройти, это если бы у них был один и тот же вид ошибки; что не невозможно, но кажется маловероятным.

### Техническое объяснение

Мы используем пакет [testing/quick](https://golang.org/pkg/testing/quick/) из стандартной библиотеки.

Читая снизу вверх, мы предоставляем `quick.Check` функцию, которую он будет запускать для ряда случайных входных данных; если функция возвращает `false`, это будет считаться провалом проверки.

Наша функция `assertion` выше принимает случайное число и запускает наши функции для проверки свойства.

### Запускаем наш тест

Попробуйте запустить его; ваш компьютер может зависнуть на некоторое время, так что убейте его, когда вам надоест :)

Что происходит? Попробуйте добавить следующее в код утверждения.

```go
assertion := func(arabic int) bool {
	if arabic < 0 || arabic > 3999 {
		log.Println(arabic)
		return true
	}
	roman := ConvertToRoman(arabic)
	fromRoman := ConvertToArabic(roman)
	return fromRoman == arabic
}
```

Вы должны увидеть что-то вроде этого:

```console
=== RUN   TestPropertiesOfConversion
2019/07/09 14:41:27 6849766357708982977
2019/07/09 14:41:27 -7028152357875163913
2019/07/09 14:41:27 -6752532134903680693
2019/07/09 14:41:27 4051793897228170080
2019/07/09 14:41:27 -1111868396280600429
2019/07/09 14:41:27 8851967058300421387
2019/07/09 14:41:27 562755830018219185
```

Всего лишь запуск этого очень простого свойства выявил недостаток в нашей реализации. Мы использовали `int` в качестве входного типа, но:

* Вы не можете использовать отрицательные числа с римскими цифрами
* Учитывая наше правило о максимуме в 3 последовательных символа, мы не можем представить значение больше 3999 ([ну, как бы](https://www.quora.com/Which-is-the-maximum-number-in-Roman-numerals)), а `int` имеет гораздо более высокое максимальное значение, чем 3999.

Это отлично! Мы были вынуждены глубже задуматься о нашем домене, что является настоящей сильной стороной тестирования на основе свойств.

Очевидно, что `int` — не самый подходящий тип. Что если мы попробуем что-то более подходящее?

### [`uint16`](https://golang.org/pkg/builtin/#uint16)

В Go есть типы для *беззнаковых целых чисел*, что означает, что они не могут быть отрицательными; это сразу же исключает один класс ошибок в нашем коде. Добавление 16 означает, что это 16-битное целое число, которое может хранить максимум `65535`, что всё ещё слишком много, но приближает нас к тому, что нам нужно.

Попробуйте обновить код, чтобы использовать `uint16` вместо `int`. Я обновил `assertion` в тесте, чтобы обеспечить немного большую видимость.

> Обратите внимание, что вам также необходимо изменить тип переменной `arabic` на `uint16` в вашем коде (об этом сообщит тест). Что может потребовать больше усилий, так это ошибка, которую вы получите для строки `arabic += numeral.Value`. Вы получаете эту ошибку, потому что мы объявили `arabic` в `ConvertToArabic` с `var arabic = 0`. Это объявление корректно, но предполагает, что `0` должен рассматриваться как значение типа `int`. Это не сработает, если вы попытаетесь сложить значение типа `int` и значение типа `uint16`. Поскольку Go является типизированным языком, это не будет работать. Поэтому не забудьте изменить `var arabic = 0` на `var arabic uint16 = 0`, чтобы сделать переменную `arabic` типа `uint16`.

```go
assertion := func(arabic uint16) bool {
	t.Log("testing", arabic)
	roman := ConvertToRoman(arabic)
	fromRoman := ConvertToArabic(roman)
	return fromRoman == arabic
}
```

Обратите внимание, что теперь мы логируем ввод, используя метод `log` из фреймворка тестирования. Убедитесь, что вы запускаете команду `go test` с флагом `-v` для вывода дополнительной информации (`go test -v`).

Если вы запустите тест, то теперь он действительно будет выполняться, и вы сможете увидеть, что тестируется. Это дает мне большую уверенность в том, что наш код работает так, как мы хотим.

Но все еще есть тонкий пробел: `uint16` доходит до 65535, а значения выше 3999 не являются действительными римскими цифрами. Вы все равно будете передавать значения в этом диапазоне — и когда наши функции их не обрабатывают, преобразование туда и обратно молча выдает неверный результат, который мы не отлавливаем. Вы могли бы защититься от этого с помощью `if arabic > 3999 { return true }`, но это лишь затушевывает проблему: эти итерации становятся пустыми операциями.

Лучший подход — указать `quick.Check` генерировать только те значения, которые нас действительно интересуют. Мы можем сделать это с помощью поля `Values` в `quick.Config`:

```go
if err := quick.Check(assertion, &quick.Config{
	MaxCount: 1000,
	Values: func(args []reflect.Value, r *rand.Rand) {
		args[0] = reflect.ValueOf(uint16(r.Intn(4000)))
	},
}); err != nil {
	t.Error("failed checks", err)
}
```

`Values` — это функция, которая принимает срез значений аргументов, которые `quick.Check` передаст вашему утверждению, и источник случайных чисел. Мы заполняем `args[0]` значением `uint16`, взятым из диапазона `[0, 3999]`. Теперь каждый из наших 1000 запусков фактически проверяет преобразование — никаких потраченных впустую итераций.

### Дальнейшая работа

* Можете ли вы написать property-тесты, которые проверяют другие описанные нами свойства?
* Можете ли вы придумать способ сделать так, чтобы кто-то не мог вызвать наш код с числом, большим 3999?
  * Вы могли бы вернуть ошибку
  * Или создать новый тип, который не может представлять числа > 3999
    * Что, по вашему мнению, лучше?

## Заключение

### Больше практики TDD с итеративной разработкой

Показалась ли вам сначала пугающей мысль о написании кода, который преобразует 1984 в MCMLXXXIV? Мне — да, и я пишу программное обеспечение довольно давно.

Хитрость, как всегда, заключается в том, чтобы **начать с чего-то простого** и делать **маленькие шаги**.

Ни на одном этапе этого процесса мы не делали больших скачков, не проводили огромных рефакторингов и не попадали в беспорядок.

Я слышу, как кто-то цинично говорит: "это просто ката". Я не могу с этим спорить, но я все равно использую тот же подход для каждого проекта, над которым работаю. Я никогда не выпускаю большую распределенную систему на первом шаге, я нахожу самое простое, что команда могла бы выпустить (обычно веб-сайт "Hello world"), а затем итерирую по небольшим частям функциональности управляемыми фрагментами, точно так же, как мы делали здесь.

Навык заключается в знании того, *как* разделять работу, и это приходит с практикой и с помощью прекрасного TDD.

### Тесты на основе свойств

* Встроены в стандартную библиотеку
* Если вы можете придумать способы описания правил вашего домена в коде, они являются отличным инструментом для повышения вашей уверенности.
* Заставляют вас глубоко задуматься о вашем домене
* Потенциально хорошее дополнение к вашему набору тестов

## Постскриптум

Эта книга зависит от ценных отзывов сообщества. [Дэйв](http://github.com/gypsydave5) оказывает огромную помощь практически в каждой главе. Но он по-настоящему возмущался моим использованием "арабских цифр" в этой главе, поэтому, в интересах полного раскрытия информации, вот что он сказал.

> Просто напишу, почему значение типа `int` на самом деле не является "арабской цифрой". Возможно, я слишком точен, поэтому я полностью пойму, если вы скажете мне отстать.
>
> *Цифра* — это символ, используемый для представления чисел — от латинского слова "палец", поскольку их у нас обычно десять. В арабской (также называемой индо-арабской) системе счисления их десять.
>
> Эти арабские цифры:
>
> ```console
>   0 1 2 3 4 5 6 7 8 9
> ```
>
> *Число* (numeral) — это представление числа с использованием набора цифр. Арабское число — это число, представленное арабскими цифрами в позиционной системе счисления по основанию 10. Мы говорим "позиционная", потому что каждая цифра имеет различное значение в зависимости от ее позиции в числе. Так
>
> ```console
>   1337
> ```
>
> `1` имеет значение одна тысяча, потому что это первая цифра в четырехзначном числе.
>
> Римские цифры строятся с использованием уменьшенного количества символов (`I`, `V` и т.д.) в основном как значения для получения числа. Есть немного позиционного подхода, но в основном `I` всегда представляет "один".
>
> Итак, учитывая это, является ли `int` "арабским числом"? Идея числа вообще не связана с его представлением — мы можем убедиться в этом, если спросим себя, каково правильное представление этого числа:
>
> ```console
> 255
> 11111111
> two-hundred and fifty-five
> FF
> 377
> ```
>
> Да, это вопрос с подвохом. Все они верны. Это представление одного и того же числа в десятичной, двоичной, английской, шестнадцатеричной и восьмеричной системах счисления соответственно.
>
> Представление числа в виде цифры *независимо* от его свойств как числа — и мы можем видеть это, когда смотрим на целочисленные литералы в Go:
>
> ```go
> 	0xFF == 255 // true
> ```
>
> И как мы можем выводить целые числа в форматированной строке:
>
> ```go
> n := 255
> fmt.Printf("%b %c %d %o %q %x %X %U", n, n, n, n, n, n, n, n)
> // 11111111 ÿ 255 377 'ÿ' ff FF U+00FF
> ```
>
> Мы можем записать одно и то же целое число как в шестнадцатеричной, так и в арабской (десятичной) системах счисления.
>
> Поэтому, когда сигнатура функции выглядит как `ConvertToRoman(arabic int) string`, это делает некоторое предположение о том, как она будет вызываться. Потому что иногда `arabic` будет записано как десятичный целочисленный литерал
>
> ```go
> 	ConvertToRoman(255)
> ```
>
> Но оно вполне могло бы быть записано и так
>
> ```go
> 	ConvertToRoman(0xFF)
> ```
>
> На самом деле, мы вовсе не "конвертируем" из арабского числа, мы "печатаем" — представляем — `int` как римское число — а `int`'ы не являются числами, арабскими или какими-либо еще; это просто числа. Функция `ConvertToRoman` больше похожа на `strconv.Itoa` в том смысле, что она превращает `int` в `string`.
>
> Но всем остальным версиям ката на это различие все равно, так что :person\_shrugging:

***


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://eda-1.gitbook.io/lgwt/osnovy-go/roman-numerals.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
