> 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-testirovaniya/scaling-acceptance-tests.md).

# Масштабирование приемочных тестов

Эта глава является продолжением [Введения в приемочные тесты](https://quii.gitbook.io/learn-go-with-tests/testing-fundamentals/intro-to-acceptance-tests). [Готовый код для этой главы можно найти на GitHub](https://github.com/quii/go-specs-greet).

Приемочные тесты (AT) имеют решающее значение, и они напрямую влияют на вашу способность уверенно развивать свою систему с течением времени при разумной стоимости изменений.

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

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

## Необходимые материалы

Вдохновение для этой главы родилось из многолетнего разочарования в приемочных тестах. Два видео, которые я рекомендую посмотреть:

* Дэйв Фарли (Dave Farley) - [Как писать приемочные тесты](https://www.youtube.com/watch?v=JDD5EEJgpHU)
* Нэт Прайс (Nat Pryce) - [E2E функциональные тесты, которые могут выполняться за миллисекунды](https://www.youtube.com/watch?v=Fk4rCn4YLLU)

"Growing Object Oriented Software" (GOOS) — очень важная книга для многих разработчиков программного обеспечения, включая меня. Подход, который она предписывает, я рекомендую своим коллегам-инженерам.

* [GOOS](http://www.growing-object-oriented-software.com) - Нэт Прайс и Стив Фриман (Nat Pryce & Steve Freeman)

Наконец, [Рия Даттани (Riya Dattani)](https://twitter.com/dattaniriya) и я обсуждали эту тему в контексте BDD в нашей лекции [Приемочные тесты, BDD и Go](https://www.youtube.com/watch?v=ZMWJCk_0WrY).

## Краткий обзор

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

## Анатомия плохих приемочных тестов

За многие годы я работал в нескольких компаниях и командах. Каждая из них осознавала необходимость приемочных тестов; какой-то способ тестирования системы с точки зрения пользователя и проверки ее работоспособности, но почти без исключений стоимость этих тестов становилась для команды настоящей проблемой.

* Медленно выполняются
* Хрупкие
* Нестабильные (flaky)
* Дорогие в поддержке, и, похоже, усложняют изменение программного обеспечения больше, чем следовало бы
* Могут работать только в определенной среде, что приводит к медленным и некачественным циклам обратной связи

Допустим, вы собираетесь написать приемочный тест для веб-сайта, который вы создаете. Вы решаете использовать безголовый веб-браузер (например, [Selenium](https://www.selenium.dev)) для имитации нажатия пользователем кнопок на вашем веб-сайте, чтобы проверить, делает ли он то, что должен.

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

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

### Жесткая связанность

Подумайте, что побуждает к изменению приемочных тестов:

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

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

![Рия и я говорим о разделении ответственности в наших тестах](https://i.imgur.com/bbG6z57.png)

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

## Анатомия хороших приемочных тестов

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

### О типах сложности

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

* **Случайная сложность** (Accidental complexity) — это сложность, с которой мы должны иметь дело, потому что мы работаем с компьютерами, например, сети, диски, API и т.д.
* **Существенная сложность** (Essential complexity) иногда называется "доменной логикой". Это конкретные правила и истины в вашей предметной области.
  * Например, "если владелец счета снимает больше денег, чем доступно, у него возникает овердрафт". Это утверждение ничего не говорит о компьютерах; это утверждение было верным еще до того, как компьютеры стали использоваться в банках!

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

### Разделение ответственности

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

Эта идея должна показаться вам разумной. В продакшн-коде мы часто стремимся разделить ответственность и отвязать единицы работы. Не колеблясь, вы введете `interface`, чтобы ваш `HTTP` обработчик мог быть отвязан от не-HTTP проблем? Давайте применим этот же подход к нашим приемочным тестам.

Дэйв Фарли описывает конкретную структуру.

![Дэйв Фарли о приемочных тестах](https://i.imgur.com/nPwpihG.png)

На GopherconUK мы с Рией представили это в терминах Go.

![Разделение ответственности](https://i.imgur.com/qdY4RJe.png)

### Тестирование на стероидах

Отделение способа выполнения спецификации позволяет нам повторно использовать ее в различных сценариях. Мы можем:

#### Сделать наши драйверы настраиваемыми

Это означает, что вы можете запускать свои AT локально, в вашей тестовой (staging) и (в идеале) производственной среде.

* Слишком многие команды проектируют свои системы таким образом, что приемочные тесты невозможно запустить локально. Это приводит к недопустимо медленному циклу обратной связи. Разве вы не предпочли бы быть уверенными, что ваши AT пройдут *до* интеграции вашего кода? Если тесты начинают ломаться, приемлемо ли, что вы не сможете воспроизвести ошибку локально, и вместо этого придется коммитить изменения и скрещивать пальцы, что они пройдут через 20 минут в другой среде?
* Помните, что то, что ваши тесты проходят на стейджинге, не означает, что ваша система будет работать. Совместимость Dev/Prod, в лучшем случае, — белая ложь. [Я тестирую в продакшене](https://increment.com/testing/i-test-in-production/).
* Всегда существуют различия между средами, которые могут влиять на *поведение* вашей системы. CDN может иметь некорректно настроенные заголовки кэша; зависимая служба, от которой вы зависите, может вести себя по-другому; значение конфигурации может быть неверным. Но разве не было бы здорово, если бы вы могли запускать свои спецификации в продакшене, чтобы быстро ловить эти проблемы?

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

Эта гибкость позволяет нам тестировать поведение на разных уровнях абстракции и архитектуры, что позволяет нам иметь более сфокусированные тесты, выходящие за рамки тестов "черного ящика".

* Например, у вас может быть веб-страница, за которой стоит API. Почему бы не использовать ту же спецификацию для тестирования обоих? Вы можете использовать безголовый веб-браузер для веб-страницы и HTTP-вызовы для API.
* Развивая эту идею, в идеале мы хотим, чтобы **код моделировал существенную сложность** (как "доменный" код), поэтому мы также должны иметь возможность использовать наши спецификации для модульных тестов. Это даст быструю обратную связь о том, что существенная сложность в нашей системе смоделирована и ведет себя правильно.

### Приемочные тесты меняются по правильным причинам

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

* Если ваш HTTP API должен измениться, у вас есть одно очевидное место для его обновления — драйвер.
* Если ваша разметка изменится, опять же, обновите конкретный драйвер.

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

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

### Приемочные тесты как метод разработки программного обеспечения

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

Впервые я познакомился с этим способом работы в GOOS. Некоторое время назад я изложил идеи в своем блоге. Вот выдержка из моего поста [Почему TDD](https://quii.dev/The_Why_of_TDD)

***

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

Следуйте "нисходящему" подходу, начиная с приемочного теста (AT), который проверяет поведение извне. Это будет служить путеводной звездой для ваших усилий. Все, на чем вы должны сосредоточиться, — это заставить этот тест пройти. Этот тест, скорее всего, будет падать какое-то время, пока вы не разработаете достаточно кода, чтобы он прошел.

![](https://i.imgur.com/pxTaYu4.png)

Как только ваш AT настроен, вы можете перейти к процессу TDD, чтобы получить достаточно модулей для прохождения AT. Хитрость заключается в том, чтобы не слишком беспокоиться о дизайне на этом этапе; получите достаточно кода, чтобы AT прошел, потому что вы всё ещё учитесь и исследуете проблему.

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

![](https://i.imgur.com/t5y5opw.png)

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

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

![](https://i.imgur.com/UYqd7Cq.png)

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

#### Опасности подхода "снизу вверх"

Это "нисходящий" подход, а не "восходящий". Подход "снизу вверх" имеет свои применения, но он несет элемент риска. Создавая "сервисы" и код в изоляции, "снизу вверх", без быстрой интеграции в ваше приложение и без проверки высокоуровневым тестом, **вы рискуете потратить много усилий на невалидированные идеи**.

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

Слишком часто я сталкивался с инженерами, которые создавали фрагмент кода, изолированно, "снизу вверх", который, как они думали, решит задачу, но он:

* Не работает так, как нам нужно
* Делает то, что нам не нужно
* Не интегрируется легко
* В любом случае требует тонны переписывания

Это пустая трата ресурсов.

## Достаточно разговоров, время кодить

В отличие от других глав, вам понадобится установленный [Docker](https://www.docker.com), потому что мы будем запускать наши приложения в контейнерах. Предполагается, что на данном этапе книги вы свободно пишете Go-код, импортируете из разных пакетов и т. д.

Создайте новый проект с помощью `go mod init github.com/quii/go-specs-greet` (вы можете указать здесь что угодно, но если вы измените путь, вам нужно будет изменить все внутренние импорты, чтобы они соответствовали ему).

Создайте папку `specifications` для хранения наших спецификаций и добавьте файл `greet.go`.

```go
package specifications

import (
	"testing"

	"github.com/alecthomas/assert/v2"
)

type Greeter interface {
	Greet() (string, error)
}

func GreetSpecification(t testing.TB, greeter Greeter) {
	got, err := greeter.Greet()
	assert.NoError(t, err)
	assert.Equal(t, got, "Hello, world")
}
```

Моя IDE (Goland) берет на себя возню с добавлением зависимостей, но если вам нужно сделать это вручную, вы бы сделали:

`go get github.com/alecthomas/assert/v2`

Учитывая дизайн приемочных тестов Фарли (Спецификация->DSL->Драйвер->Система), у нас теперь есть спецификация, отделенная от реализации. Она не знает и не заботится о том, *как* мы `Greet`; она занимается только существенной сложностью нашей предметной области. Признаться, сейчас эта сложность невелика, но мы расширим спецификацию, чтобы добавить больше функциональности по мере дальнейших итераций. Всегда важно начинать с малого!

Вы можете рассматривать интерфейс как наш первый шаг к DSL; по мере роста проекта вы можете обнаружить необходимость в иной абстракции, но пока это нормально.

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

Мы можем использовать эту спецификацию для проверки любой "системы", которая может `Greet`.

### Первая система: HTTP API

Нам требуется предоставить "сервис приветствия" через HTTP. Поэтому нам нужно создать:

1. **Драйвер**. В данном случае это драйвер, который работает с HTTP-системой, используя **HTTP-клиент**. Этот код будет знать, как работать с нашим API. Драйверы переводят DSL в специфические для системы вызовы; в нашем случае драйвер будет реализовывать интерфейс, определенный спецификациями.
2. **HTTP-сервер** с API приветствия
3. **Тест**, который отвечает за управление жизненным циклом запуска сервера, а затем подключение драйвера к спецификации для запуска его в качестве теста.

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

Начальный процесс создания черноящичного теста, который компилирует и запускает вашу программу, выполняет тест, а затем все очищает, может быть довольно трудоемким. Вот почему предпочтительнее делать это в начале проекта с минимальной функциональностью. Я обычно начинаю все свои проекты с реализации сервера "hello world", со всеми настроенными тестами, готовыми для быстрого создания фактической функциональности.

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

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

`mkdir -p cmd/httpserver`

Внутри новой папки создайте новый файл `greeter_server_test.go` и добавьте следующее.

```go
package main_test

import (
	"testing"

	"github.com/quii/go-specs-greet/specifications"
)

func TestGreeterServer(t *testing.T) {
	specifications.GreetSpecification(t, nil)
}
```

Мы хотим запустить нашу спецификацию в Go-тесте. У нас уже есть доступ к `*testing.T`, так что это первый аргумент, но что насчет второго?

`specifications.Greeter` — это интерфейс, который мы реализуем с помощью `Driver`, изменив новый код `TestGreeterServer` на следующий:

```go
import (
	go_specs_greet "github.com/quii/go-specs-greet"
)

func TestGreeterServer(t *testing.T) {
	driver := go_specs_greet.Driver{BaseURL: "http://localhost:8080"}
	specifications.GreetSpecification(t, driver)
}
```

Было бы желательно, чтобы наш `Driver` был конфигурируемым для запуска в различных средах, включая локальную, поэтому мы добавили поле `BaseURL`.

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

```
./greeter_server_test.go:46:12: undefined: go_specs_greet.Driver
```

Мы всё ещё практикуем TDD! Это большой первый шаг, который нам предстоит сделать; нам нужно создать несколько файлов и, возможно, написать больше кода, чем мы привыкли, но когда вы только начинаете, это часто бывает так. Очень важно помнить правила красного шага.

> Совершите столько грехов, сколько необходимо, чтобы тест прошел.

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

Задержите дыхание; помните, мы можем провести рефакторинг, когда тест пройдет. Вот код для драйвера в `driver.go`, который мы поместим в корень проекта:

```go
package go_specs_greet

import (
	"io"
	"net/http"
)

type Driver struct {
	BaseURL string
}

func (d Driver) Greet() (string, error) {
	res, err := http.Get(d.BaseURL + "/greet")
	if err != nil {
		return "", err
	}
	defer res.Body.Close()
	greeting, err := io.ReadAll(res.Body)
	if err != nil {
		return "", err
	}
	return string(greeting), nil
}
```

Примечания:

* Можно утверждать, что я должен писать тесты для проверки различных `if err != nil`, но по моему опыту, пока вы ничего не делаете с `err`, тесты, которые говорят "вы возвращаете ошибку, которую получаете", имеют относительно низкую ценность.
* **Вам не следует использовать HTTP-клиент по умолчанию**. Позже мы передадим HTTP-клиент для его настройки с таймаутами и т. д., но сейчас мы просто пытаемся добиться прохождения теста.
* В нашем `greeter_server_test.go` мы вызвали функцию Driver из пакета `go_specs_greet`, который мы только что создали, не забудьте добавить `github.com/quii/go-specs-greet` в его импорты. Попробуйте снова запустить тесты; они должны скомпилироваться, но не пройти.

```
Get "http://localhost:8080/greet": dial tcp [::1]:8080: connect: connection refused
```

У нас есть `Driver`, но мы еще не запустили наше приложение, поэтому оно не может выполнить HTTP-запрос. Наш приемочный тест должен координировать сборку, запуск и, наконец, остановку нашей системы для выполнения теста.

### Запуск нашего приложения

Обычно команды создают Docker-образы своих систем для развертывания, поэтому для нашего теста мы сделаем то же самое.

Чтобы помочь нам использовать Docker в наших тестах, мы будем использовать [Testcontainers](https://golang.testcontainers.org). Testcontainers предоставляет нам программный способ создания Docker-образов и управления жизненными циклами контейнеров.

`go get github.com/testcontainers/testcontainers-go`

Теперь вы можете отредактировать `cmd/httpserver/greeter_server_test.go` следующим образом:

```go
package main_test

import (
	"context"
	"testing"

	"github.com/alecthomas/assert/v2"
	go_specs_greet "github.com/quii/go-specs-greet"
	"github.com/quii/go-specs-greet/specifications"
	"github.com/testcontainers/testcontainers-go"
	"github.com/testcontainers/testcontainers-go/wait"
)

func TestGreeterServer(t *testing.T) {
	ctx := context.Background()

	req := testcontainers.ContainerRequest{
		FromDockerfile: testcontainers.FromDockerfile{
			Context:    "../../.",
			Dockerfile: "./cmd/httpserver/Dockerfile",
			// set to false if you want less spam, but this is helpful if you're having troubles
			PrintBuildLog: true,
		},
		ExposedPorts: []string{"8080:8080"},
		WaitingFor:   wait.ForHTTP("/").WithPort("8080"),
	}
	container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
		ContainerRequest: req,
		Started:          true,
	})
	assert.NoError(t, err)
	t.Cleanup(func() {
		assert.NoError(t, container.Terminate(ctx))
	})

	driver := go_specs_greet.Driver{BaseURL: "http://localhost:8080"}
	specifications.GreetSpecification(t, driver)
}
```

Попробуйте запустить тест.

```
=== RUN   TestGreeterHandler
2022/09/10 18:49:44 Starting container id: 03e8588a1be4 image: docker.io/testcontainers/ryuk:0.3.3
2022/09/10 18:49:45 Waiting for container id 03e8588a1be4 image: docker.io/testcontainers/ryuk:0.3.3
2022/09/10 18:49:45 Container is ready id: 03e8588a1be4 image: docker.io/testcontainers/ryuk:0.3.3
    greeter_server_test.go:32: Did not expect an error but got:
        Error response from daemon: Cannot locate specified Dockerfile: ./cmd/httpserver/Dockerfile: failed to create container
--- FAIL: TestGreeterHandler (0.59s)
```

Нам нужно создать Dockerfile для нашей программы. Внутри папки `httpserver` создайте `Dockerfile` и добавьте следующее.

```dockerfile
# Make sure to specify the same Go version as the one in the go.mod file.
# For example, golang:1.22.1-alpine.
FROM golang:1.18-alpine

WORKDIR /app

COPY go.mod ./

RUN go mod download

COPY . .

RUN go build -o svr cmd/httpserver/*.go

EXPOSE 8080
CMD [ "./svr" ]
```

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

Попробуйте снова запустить тест; он должен пожаловаться на невозможность сборки образа. Конечно, это потому, что мы еще не написали программу для сборки!

Чтобы тест полностью выполнился, нам нужно будет создать программу, которая слушает на `8080`, но **это все**. Придерживайтесь дисциплины TDD, не пишите production-код, который заставил бы тест пройти, пока мы не убедимся, что тест падает, как мы и ожидали.

Создайте `main.go` в нашей папке `httpserver` со следующим содержимым:

```go
package main

import (
	"log"
	"net/http"
)

func main() {
	handler := http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
	})
	if err := http.ListenAndServe(":8080", handler); err != nil {
		log.Fatal(err)
	}
}
```

Попробуйте снова запустить тест, и он должен завершиться неудачей со следующим результатом.

```
    greet.go:16: Expected values to be equal:
        +Hello, World
        \ No newline at end of file
--- FAIL: TestGreeterHandler (2.09s)
```

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

Обновите обработчик, чтобы он вел себя так, как того требует наша спецификация.

```go
import (
	"fmt"
	"log"
	"net/http"
)

func main() {
	handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		fmt.Fprint(w, "Hello, world")
	})
	if err := http.ListenAndServe(":8080", handler); err != nil {
		log.Fatal(err)
	}
}
```

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

Хотя технически это не рефакторинг, мы не должны полагаться на HTTP-клиент по умолчанию, поэтому давайте изменим наш `Driver`, чтобы мы могли передавать его, что и будет делать наш тест.

```go
import (
	"io"
	"net/http"
)

type Driver struct {
	BaseURL string
	Client  *http.Client
}

func (d Driver) Greet() (string, error) {
	res, err := d.Client.Get(d.BaseURL + "/greet")
	if err != nil {
		return "", err
	}
	defer res.Body.Close()
	greeting, err := io.ReadAll(res.Body)
	if err != nil {
		return "", err
	}
	return string(greeting), nil
}
```

В нашем тесте в `cmd/httpserver/greeter_server_test.go` обновите создание драйвера, чтобы передать клиент.

```go
client := http.Client{
	Timeout: 1 * time.Second,
}

driver := go_specs_greet.Driver{BaseURL: "http://localhost:8080", Client: &client}
specifications.GreetSpecification(t, driver)
```

Хорошей практикой является сохранение `main.go` максимально простым; он должен быть озабочен только сборкой строительных блоков, которые вы создаете, в приложение.

Создайте файл в корне проекта с именем `handler.go` и переместите туда наш код.

```go
package go_specs_greet

import (
	"fmt"
	"net/http"
)

func Handler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprint(w, "Hello, world")
}
```

Обновите `main.go`, чтобы он импортировал и использовал обработчик вместо этого.

```go
package main

import (
	"net/http"

	go_specs_greet "github.com/quii/go-specs-greet"
)

func main() {
	handler := http.HandlerFunc(go_specs_greet.Handler)
	http.ListenAndServe(":8080", handler)
}
```

## Размышления

Первый шаг показался трудным. Мы создали несколько файлов `go` для создания и тестирования HTTP-обработчика, который возвращает жестко закодированную строку. Эта "итерация 0" церемонии и настройки послужит нам хорошо для дальнейших итераций.

Изменение функциональности должно быть простым и контролироваться за счет продвижения ее через спецификацию и работы с любыми изменениями, которые она заставляет нас делать. Теперь, когда `DockerFile` и `testcontainers` настроены для нашего приемочного теста; нам не придется изменять эти файлы, если только не изменится способ построения нашего приложения.

Мы увидим это с нашим следующим требованием: приветствовать конкретного человека.

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

Отредактируйте нашу спецификацию

```go
package specifications

import (
	"testing"

	"github.com/alecthomas/assert/v2"
)

type Greeter interface {
	Greet(name string) (string, error)
}

func GreetSpecification(t testing.TB, greeter Greeter) {
	got, err := greeter.Greet("Mike")
	assert.NoError(t, err)
	assert.Equal(t, got, "Hello, Mike")
}
```

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

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

```
./greeter_server_test.go:48:39: cannot use driver (variable of type go_specs_greet.Driver) as type specifications.Greeter in argument to specifications.GreetSpecification:
	go_specs_greet.Driver does not implement specifications.Greeter (wrong type for Greet method)
		have Greet() (string, error)
		want Greet(name string) (string, error)
```

Изменение в спецификации означает, что наш драйвер нуждается в обновлении.

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

Обновите драйвер так, чтобы он указывал значение запроса `name` в запросе, чтобы запросить приветствие определенного `name`.

```go
import "io"

func (d Driver) Greet(name string) (string, error) {
	res, err := d.Client.Get(d.BaseURL + "/greet?name=" + name)
	if err != nil {
		return "", err
	}
	defer res.Body.Close()
	greeting, err := io.ReadAll(res.Body)
	if err != nil {
		return "", err
	}
	return string(greeting), nil
}
```

Тест теперь должен запуститься и завершиться неудачей.

```
    greet.go:16: Expected values to be equal:
        -Hello, world
        \ No newline at end of file
        +Hello, Mike
        \ No newline at end of file
--- FAIL: TestGreeterHandler (1.92s)
```

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

Извлеките `name` из запроса и поприветствуйте.

```go
import (
	"fmt"
	"net/http"
)

func Handler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Hello, %s", r.URL.Query().Get("name"))
}
```

Тест теперь должен пройти.

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

В [HTTP Handlers Revisited](https://github.com/quii/learn-go-with-tests/blob/main/http-handlers-revisited.md) мы обсуждали, насколько важно, чтобы HTTP-обработчики отвечали только за обработку HTTP-аспектов; любая "доменная логика" должна находиться за пределами обработчика. Это позволяет нам разрабатывать доменную логику в изоляции от HTTP, что упрощает ее тестирование и понимание.

Давайте разделим эти аспекты.

Обновите наш обработчик в `./handler.go` следующим образом:

```go
func Handler(w http.ResponseWriter, r *http.Request) {
	name := r.URL.Query().Get("name")
	fmt.Fprint(w, Greet(name))
}
```

Создайте новый файл `./greet.go`:

```go
package go_specs_greet

import "fmt"

func Greet(name string) string {
	return fmt.Sprintf("Hello, %s", name)
}
```

## Небольшое отступление в шаблон проектирования "адаптер"

Теперь, когда мы отделили нашу доменную логику приветствия людей в отдельную функцию, мы можем писать модульные тесты для нашей функции `Greet`. Это, несомненно, намного проще, чем тестировать ее через спецификацию, которая проходит через драйвер, обращающийся к веб-серверу, чтобы получить строку!

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

Давайте попробуем, создав `./greet_test.go` следующим образом:

```go
package go_specs_greet_test

import (
	"testing"

	go_specs_greet "github.com/quii/go-specs-greet"
	"github.com/quii/go-specs-greet/specifications"
)

func TestGreet(t *testing.T) {
	specifications.GreetSpecification(t, go_specs_greet.Greet)
}

```

Это было бы хорошо, но это не работает.

```
./greet_test.go:11:39: cannot use go_specs_greet.Greet (value of type func(name string) string) as type specifications.Greeter in argument to specifications.GreetSpecification:
	func(name string) string does not implement specifications.Greeter (missing Greet method)
```

Наша спецификация хочет что-то, что имеет метод `Greet()`, а не функцию.

Ошибка компиляции разочаровывает; у нас есть нечто, что мы "знаем" как `Greeter`, но оно не совсем в нужной **форме**, чтобы компилятор позволил нам его использовать. Именно для этого и предназначен шаблон **адаптер**.

> В [программной инженерии](https://en.wikipedia.org/wiki/Software_engineering) **шаблон адаптер** — это [шаблон проектирования программного обеспечения](https://en.wikipedia.org/wiki/Software_design_pattern) (также известный как [обертка](https://en.wikipedia.org/wiki/Wrapper_function), альтернативное название, используемое также для [шаблона декоратор](https://en.wikipedia.org/wiki/Decorator_pattern)), который позволяет использовать [интерфейс](https://en.wikipedia.org/wiki/Interface_\(computer_science\)) существующего [класса](https://en.wikipedia.org/wiki/Class_\(computer_science\)) в качестве другого интерфейса.\[[1\]](https://en.wikipedia.org/wiki/Adapter_pattern#cite_note-HeadFirst-1) Он часто используется для обеспечения взаимодействия существующих классов с другими без изменения их [исходного кода](https://en.wikipedia.org/wiki/Source_code).

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

Добавьте этот код в `./specifications/adapters.go`

```go
type GreetAdapter func(name string) string

func (g GreetAdapter) Greet(name string) (string, error) {
	return g(name), nil
}
```

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

```go
package go_specs_greet_test

import (
	"testing"

	gospecsgreet "github.com/quii/go-specs-greet"
	"github.com/quii/go-specs-greet/specifications"
)

func TestGreet(t *testing.T) {
	specifications.GreetSpecification(
		t,
		specifications.GreetAdapter(gospecsgreet.Greet),
	)
}
```

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

## Размышления

Изменение поведения казалось простым, верно? Ладно, возможно, это было просто из-за характера проблемы, но этот метод работы дает вам дисциплину и простой, повторяемый способ изменения вашей системы сверху донизу:

* Проанализируйте свою проблему и определите небольшое улучшение вашей системы, которое продвинет вас в правильном направлении
* Зафиксируйте новую существенную сложность в спецификации
* Следуйте ошибкам компиляции, пока AT не запустится
* Обновите свою реализацию, чтобы система вела себя в соответствии со спецификацией
* Проведите рефакторинг

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

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

```
quii@Chriss-MacBook-Pro go-specs-greet % go test ./...
ok  	github.com/quii/go-specs-greet	0.181s
ok  	github.com/quii/go-specs-greet/cmd/httpserver	2.221s
?   	github.com/quii/go-specs-greet/specifications	[no test files]
```

Теперь представьте, что ваш CTO решил, что gRPC — это *будущее*. Она хочет, чтобы вы предоставили ту же функциональность через gRPC-сервер, сохраняя при этом существующий HTTP-сервер.

Это пример **случайной сложности**. Помните, случайная сложность — это сложность, с которой мы сталкиваемся из-за работы с компьютерами, такими как сети, диски, API и т. д. **Существенная сложность не изменилась**, поэтому нам не нужно менять наши спецификации.

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

### Облегчение изменения

Иногда имеет смысл провести некоторый рефакторинг *перед* внесением изменений.

> Сначала сделайте изменение легким, затем сделайте легкое изменение.

\~Кент Бек

По этой причине давайте переместим наш `http`-код — `driver.go` и `handler.go` — в пакет с именем `httpserver` внутри папки `adapters` и изменим их имена пакетов на `httpserver`.

Теперь вам нужно будет импортировать корневой пакет в `handler.go`, чтобы сослаться на метод `Greet`...

```go
package httpserver

import (
	"fmt"
	"net/http"

	go_specs_greet "github.com/quii/go-specs-greet/domain/interactions"
)

func Handler(w http.ResponseWriter, r *http.Request) {
	name := r.URL.Query().Get("name")
	fmt.Fprint(w, go_specs_greet.Greet(name))
}

```

импортируйте ваш адаптер httpserver в main.go:

```go
package main

import (
	"net/http"

	"github.com/quii/go-specs-greet/adapters/httpserver"
)

func main() {
	handler := http.HandlerFunc(httpserver.Handler)
	http.ListenAndServe(":8080", handler)
}
```

и обновите импорт и ссылку на `Driver` в greeter\_server\_test.go:

```go
driver := httpserver.Driver{BaseURL: "http://localhost:8080", Client: &client}
```

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

Вместо того чтобы видеть

```go
domain.Greet
```

Что довольно странно, лучше отдать предпочтение

```go
interactions.Greet
```

Создайте папку `domain` для всего вашего доменного кода, а внутри нее — папку `interactions`. В зависимости от ваших инструментов вам, возможно, придется обновить некоторые импорты и код.

Наше дерево проекта теперь должно выглядеть так:

```
quii@Chriss-MacBook-Pro go-specs-greet % tree
.
├── Makefile
├── README.md
├── adapters
│   └── httpserver
│       ├── driver.go
│       └── handler.go
├── cmd
│   └── httpserver
|       ├── Dockerfile
│       ├── greeter_server_test.go
│       └── main.go
├── domain
│   └── interactions
│       ├── greet.go
│       └── greet_test.go
├── go.mod
├── go.sum
└── specifications
    └── adapters.go
    └── greet.go

```

Наш доменный код, **существенная сложность**, находится в корне нашего модуля Go, а код, который позволит нам использовать его в "реальном мире", организован в **адаптеры**. Папка `cmd` — это место, где мы можем скомпоновать эти логические группы в практические приложения, имеющие черноящичные тесты для проверки их работы. Отлично!

Наконец, мы можем немного привести в порядок наш приемочный тест. Если вы рассмотрите высокоуровневые шаги нашего приемочного теста:

* Собрать образ docker
* Дождаться, пока он начнет прослушивать *какой-либо* порт
* Создать драйвер, который понимает, как переводить DSL в системно-специфичные вызовы
* Подключить драйвер к спецификации

... вы поймете, что у нас есть те же требования для приемочного теста для gRPC-сервера!

Папка `adapters` кажется подходящим местом, поэтому внутри файла `docker.go` инкапсулируем первые два шага в функцию, которую мы повторно используем далее.

```go
package adapters

import (
	"context"
	"fmt"
	"testing"
	"time"

	"github.com/alecthomas/assert/v2"
	"github.com/docker/go-connections/nat"
	"github.com/testcontainers/testcontainers-go"
	"github.com/testcontainers/testcontainers-go/wait"
)

func StartDockerServer(
	t testing.TB,
	port string,
	dockerFilePath string,
) {
	ctx := context.Background()
	t.Helper()
	req := testcontainers.ContainerRequest{
		FromDockerfile: testcontainers.FromDockerfile{
			Context:       "../../.",
			Dockerfile:    dockerFilePath,
			PrintBuildLog: true,
		},
		ExposedPorts: []string{fmt.Sprintf("%s:%s", port, port)},
		WaitingFor:   wait.ForListeningPort(nat.Port(port)).WithStartupTimeout(5 * time.Second),
	}
	container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
		ContainerRequest: req,
		Started:          true,
	})
	assert.NoError(t, err)
	t.Cleanup(func() {
		assert.NoError(t, container.Terminate(ctx))
	})
}
```

Это дает нам возможность немного очистить наш приемочный тест.

```go
func TestGreeterServer(t *testing.T) {
	var (
		port           = "8080"
		dockerFilePath = "./cmd/httpserver/Dockerfile"
		baseURL        = fmt.Sprintf("http://localhost:%s", port)
		driver         = httpserver.Driver{BaseURL: baseURL, Client: &http.Client{
			Timeout: 1 * time.Second,
		}}
	)

	adapters.StartDockerServer(t, port, dockerFilePath)
	specifications.GreetSpecification(t, driver)
}
```

Это должно упростить написание *следующего* теста.

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

Эта новая функциональность может быть реализована путем создания нового `адаптера` для взаимодействия с нашим доменным кодом. По этой причине мы:

* Не должны менять спецификацию;
* Должны иметь возможность повторно использовать спецификацию;
* Должны иметь возможность повторно использовать доменный код.

Создайте новую папку `grpcserver` внутри `cmd`, чтобы разместить нашу новую программу и соответствующий приемочный тест. Внутри `cmd/grpc_server/greeter_server_test.go` добавьте приемочный тест, который очень похож на наш тест HTTP-сервера, не случайно, а по замыслу.

```go
package main_test

import (
	"fmt"
	"testing"

	"github.com/quii/go-specs-greet/adapters"
	"github.com/quii/go-specs-greet/adapters/grpcserver"
	"github.com/quii/go-specs-greet/specifications"
)

func TestGreeterServer(t *testing.T) {
	var (
		port           = "50051"
		dockerFilePath = "./cmd/grpcserver/Dockerfile"
		driver         = grpcserver.Driver{Addr: fmt.Sprintf("localhost:%s", port)}
	)

	adapters.StartDockerServer(t, port, dockerFilePath)
	specifications.GreetSpecification(t, &driver)
}
```

Единственные различия:

* Мы используем другой файл docker, потому что мы создаем другую программу
* Это означает, что нам понадобится новый `Driver`, который будет использовать `gRPC` для взаимодействия с нашей новой программой

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

```
./greeter_server_test.go:26:12: undefined: grpcserver
```

Мы еще не создали `Driver`, поэтому он не скомпилируется.

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

Создайте папку `grpcserver` внутри `adapters`, а внутри нее создайте `driver.go`.

```go
package grpcserver

type Driver struct {
	Addr string
}

func (d Driver) Greet(name string) (string, error) {
	return "", nil
}
```

Если вы запустите его снова, он теперь должен *скомпилироваться*, но не пройти, потому что мы не создали Dockerfile и соответствующую программу для запуска.

Создайте новый `Dockerfile` внутри `cmd/grpcserver`.

```dockerfile
# Make sure to specify the same Go version as the one in the go.mod file.
FROM golang:1.18-alpine

WORKDIR /app

COPY go.mod ./

RUN go mod download

COPY . .

RUN go build -o svr cmd/grpcserver/*.go

EXPOSE 50051
CMD [ "./svr" ]
```

И `main.go`

```go
package main

import "fmt"

func main() {
	fmt.Println("implement me")
}
```

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

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

### gRPC

Если вы не знакомы с gRPC, я бы посоветовал начать с [веб-сайта gRPC](https://grpc.io). Тем не менее, для этой главы это просто еще один вид адаптера к нашей системе, способ для других систем вызывать (**r**emote **p**rocedure **c**all) наш отличный доменный код.

Особенность заключается в том, что вы определяете "определение службы" с использованием Protocol Buffers. Затем вы генерируете код сервера и клиента из этого определения. Это работает не только для Go, но и для большинства основных языков. Это означает, что вы можете поделиться определением с другими командами в вашей компании, которые, возможно, даже не пишут на Go, и все равно сможете беспрепятственно осуществлять связь между службами.

Если вы раньше не использовали gRPC, вам потребуется установить **компилятор Protocol buffer** и некоторые **Go-плагины**. [На веб-сайте gRPC есть четкие инструкции по этому поводу](https://grpc.io/docs/languages/go/quickstart/).

В той же папке, что и наш новый драйвер, добавьте файл `greet.proto` со следующим содержимым:

```protobuf
syntax = "proto3";

option go_package = "github.com/quii/adapters/grpcserver";

package grpcserver;

service Greeter {
  rpc Greet (GreetRequest) returns (GreetReply) {}
}

message GreetRequest {
  string name = 1;
}

message GreetReply {
  string message = 1;
}
```

Чтобы понять это определение, вам не нужно быть экспертом в Protocol Buffers. Мы определяем службу с методом Greet, а затем описываем типы входящих и исходящих сообщений.

Внутри `adapters/grpcserver` запустите следующее, чтобы сгенерировать код клиента и сервера:

```
protoc --go_out=. --go_opt=paths=source_relative \
    --go-grpc_out=. --go-grpc_opt=paths=source_relative \
    greet.proto
```

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

```go
package grpcserver

import (
	"context"

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
)

type Driver struct {
	Addr string
}

func (d Driver) Greet(name string) (string, error) {
	//todo: we shouldn't redial every time we call greet, refactor out when we're green
	conn, err := grpc.Dial(d.Addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		return "", err
	}
	defer conn.Close()

	client := NewGreeterClient(conn)
	greeting, err := client.Greet(context.Background(), &GreetRequest{
		Name: name,
	})
	if err != nil {
		return "", err
	}

	return greeting.Message, nil
}
```

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

```go
package main

import (
	"context"
	"log"
	"net"

	"github.com/quii/go-specs-greet/adapters/grpcserver"
	"google.golang.org/grpc"
)

func main() {
	lis, err := net.Listen("tcp", ":50051")
	if err != nil {
		log.Fatal(err)
	}
	s := grpc.NewServer()
	grpcserver.RegisterGreeterServer(s, &GreetServer{})

	if err := s.Serve(lis); err != nil {
		log.Fatal(err)
	}
}

type GreetServer struct {
	grpcserver.UnimplementedGreeterServer
}

func (g GreetServer) Greet(ctx context.Context, request *grpcserver.GreetRequest) (*grpcserver.GreetReply, error) {
	return &grpcserver.GreetReply{Message: "fixme"}, nil
}
```

Чтобы создать наш gRPC-сервер, мы должны реализовать сгенерированный им для нас интерфейс.

```go
// GreeterServer is the server API for Greeter service.
// All implementations must embed UnimplementedGreeterServer
// for forward compatibility
type GreeterServer interface {
	Greet(context.Context, *GreetRequest) (*GreetReply, error)
	mustEmbedUnimplementedGreeterServer()
}
```

Наша функция `main`:

* Слушает на порту
* Создает `GreetServer`, который реализует интерфейс, а затем регистрирует его с помощью `grpcServer.RegisterGreeterServer` вместе с `grpc.Server`.
* Использует сервер с прослушивателем.

Не потребовалось бы больших дополнительных усилий, чтобы вызвать наш доменный код внутри `greetServer.Greet` вместо жестко закодированного `fix-me` в сообщении, но я хотел бы сначала запустить наш приемочный тест, чтобы убедиться, что все работает на транспортном уровне, и проверить вывод неудачного теста.

```
greet.go:16: Expected values to be equal:
-fixme
\ No newline at end of file
+Hello, Mike
\ No newline at end of file
```

Отлично! Мы видим, что наш драйвер способен подключиться к нашему gRPC-серверу в тесте.

Теперь вызовите наш доменный код внутри нашего `GreetServer`.

```go
type GreetServer struct {
	grpcserver.UnimplementedGreeterServer
}

func (g GreetServer) Greet(ctx context.Context, request *grpcserver.GreetRequest) (*grpcserver.GreetReply, error) {
	return &grpcserver.GreetReply{Message: interactions.Greet(request.Name)}, nil
}
```

Наконец, он проходит! У нас есть приемочный тест, который доказывает, что наш gRPC-сервер приветствий ведет себя так, как нам хотелось бы.

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

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

### Упростите main

Как и прежде, мы не хотим, чтобы `main` содержал слишком много кода. Мы можем переместить наш новый `GreetServer` в `adapters/grpcserver`, так как именно там он и должен находиться. С точки зрения связности, если мы изменим определение службы, мы хотим, чтобы "радиус поражения" изменений был ограничен этой областью нашего кода.

### Не переподключайтесь в нашем драйвере каждый раз

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

```go
package grpcserver

import (
	"context"
	"sync"

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
)

type Driver struct {
	Addr string

	connectionOnce sync.Once
	conn           *grpc.ClientConn
	client         GreeterClient
}

func (d *Driver) Greet(name string) (string, error) {
	client, err := d.getClient()
	if err != nil {
		return "", err
	}

	greeting, err := client.Greet(context.Background(), &GreetRequest{
		Name: name,
	})
	if err != nil {
		return "", err
	}

	return greeting.Message, nil
}

func (d *Driver) getClient() (GreeterClient, error) {
	var err error
	d.connectionOnce.Do(func() {
		d.conn, err = grpc.Dial(d.Addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
		d.client = NewGreeterClient(d.conn)
	})
	return d.client, err
}
```

Здесь мы показываем, как использовать [`sync.Once`](https://pkg.go.dev/sync#Once), чтобы гарантировать, что наш `Driver` попытается установить соединение с нашим сервером только один раз.

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

```
quii@Chriss-MacBook-Pro go-specs-greet % tree
.
├── Makefile
├── README.md
├── adapters
│   ├── docker.go
│   ├── grpcserver
│   │   ├── driver.go
│   │   ├── greet.pb.go
│   │   ├── greet.proto
│   │   ├── greet_grpc.pb.go
│   │   └── server.go
│   └── httpserver
│       ├── driver.go
│       └── handler.go
├── cmd
│   ├── grpcserver
│   │   ├── Dockerfile
│   │   ├── greeter_server_test.go
│   │   └── main.go
│   └── httpserver
│       ├── Dockerfile
│       ├── greeter_server_test.go
│       └── main.go
├── domain
│   └── interactions
│       ├── greet.go
│       └── greet_test.go
├── go.mod
├── go.sum
└── specifications
    └── greet.go
```

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

### Консолидация `Dockerfile`

Вы, вероятно, заметили, что два `Dockerfile` почти идентичны, за исключением пути к бинарному файлу, который мы хотим собрать.

`Dockerfile` могут принимать аргументы, что позволяет нам повторно использовать их в разных контекстах, что кажется идеальным. Мы можем удалить наши 2 Dockerfile и вместо этого иметь один в корне проекта со следующим содержанием:

```dockerfile
# Make sure to specify the same Go version as the one in the go.mod file.
FROM golang:1.18-alpine

WORKDIR /app

ARG bin_to_build

COPY go.mod ./

RUN go mod download

COPY . .

RUN go build -o svr cmd/${bin_to_build}/main.go

CMD [ "./svr" ]
```

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

```go
func StartDockerServer(
	t testing.TB,
	port string,
	binToBuild string,
) {
	ctx := context.Background()
	t.Helper()
	req := testcontainers.ContainerRequest{
		FromDockerfile: testcontainers.FromDockerfile{
			Context:    "../../.",
			Dockerfile: "Dockerfile",
			BuildArgs: map[string]*string{
				"bin_to_build": &binToBuild,
			},
			PrintBuildLog: true,
		},
		ExposedPorts: []string{fmt.Sprintf("%s:%s", port, port)},
		WaitingFor:   wait.ForListeningPort(nat.Port(port)).WithStartupTimeout(5 * time.Second),
	}
	container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
		ContainerRequest: req,
		Started:          true,
	})
	assert.NoError(t, err)
	t.Cleanup(func() {
		assert.NoError(t, container.Terminate(ctx))
	})
}
```

И, наконец, обновите наши тесты, чтобы передать образ для сборки (сделайте это для другого теста и измените `grpcserver` на `httpserver`).

```go
func TestGreeterServer(t *testing.T) {
	var (
		port   = "50051"
		driver = grpcserver.Driver{Addr: fmt.Sprintf("localhost:%s", port)}
	)

	adapters.StartDockerServer(t, port, "grpcserver")
	specifications.GreetSpecification(t, &driver)
}
```

### Разделение различных видов тестов

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

* Медленнее
* Качество обратной связи часто не так сфокусировано, как в модульном тесте
* Не помогают с внутренним качеством или дизайном

[Тестовая пирамида](https://martinfowler.com/articles/practical-test-pyramid.html) указывает нам на то, какой тип сочетания мы хотим для нашего набора тестов; вы должны прочитать пост Фаулера для более подробной информации, но очень упрощенное резюме для этого поста: "много модульных тестов и несколько приемочных тестов".

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

Предпочтительно, чтобы запуск `go test ./...` был возможен без дополнительной настройки со стороны инженера, помимо, скажем, нескольких ключевых зависимостей, таких как компилятор Go (очевидно) и, возможно, Docker.

Go предоставляет механизм, позволяющий инженерам запускать только "короткие" тесты с помощью [флага short](https://pkg.go.dev/testing#Short).

`go test -short ./...`

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

```go
if testing.Short() {
	t.Skip()
}
```

Я создал `Makefile`, чтобы показать это использование.

```makefile
build:
	golangci-lint run
	go test ./...

unit-tests:
	go test -short ./...
```

### Когда следует писать приемочные тесты?

Лучшая практика — отдавать предпочтение большому количеству быстро выполняющихся модульных тестов и нескольким приемочным тестам, но как решить, когда следует писать приемочный тест, а когда — модульные?

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

* Это граничный случай? Я бы предпочел протестировать его модульным тестом.
* Это то, о чем много говорят нетехнические специалисты? Я бы предпочел иметь большую уверенность в том, что ключевая вещь "действительно" работает, поэтому я бы добавил приемочный тест.
* Я описываю пользовательский путь, а не конкретную функцию? Приемочный тест.
* Модульные тесты дадут мне достаточную уверенность? Иногда вы используете существующий путь, который уже имеет приемочный тест, но вы добавляете другую функциональность для обработки различных сценариев из-за разных входных данных. В этом случае добавление еще одного приемочного теста увеличивает стоимость, но приносит мало пользы, поэтому я бы предпочел несколько модульных тестов.

## Итерационная работа

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

Давайте расширим наш API, чтобы включить функцию "проклятия".

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

Это совершенно новое поведение, поэтому мы должны начать с приемочного теста. В нашем файле спецификации добавьте следующее:

```go
type MeanGreeter interface {
	Curse(name string) (string, error)
}

func CurseSpecification(t *testing.T, meany MeanGreeter) {
	got, err := meany.Curse("Chris")
	assert.NoError(t, err)
	assert.Equal(t, got, "Go to hell, Chris!")
}
```

Выберите один из наших приемочных тестов и попробуйте использовать спецификацию:

```go
func TestGreeterServer(t *testing.T) {
	if testing.Short() {
		t.Skip()
	}
	var (
		port   = "50051"
		driver = grpcserver.Driver{Addr: fmt.Sprintf("localhost:%s", port)}
	)

	t.Cleanup(driver.Close)
	adapters.StartDockerServer(t, port, "grpcserver")
	specifications.GreetSpecification(t, &driver)
	specifications.CurseSpecification(t, &driver)
}
```

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

```
# github.com/quii/go-specs-greet/cmd/grpcserver_test [github.com/quii/go-specs-greet/cmd/grpcserver.test]
./greeter_server_test.go:27:39: cannot use &driver (value of type *grpcserver.Driver) as type specifications.MeanGreeter in argument to specifications.CurseSpecification:
	*grpcserver.Driver does not implement specifications.MeanGreeter (missing Curse method)
```

Наш `Driver` еще не поддерживает `Curse`.

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

Помните, мы просто пытаемся заставить тест работать, поэтому добавьте метод к `Driver`.

```go
func (d *Driver) Curse(name string) (string, error) {
	return "", nil
}
```

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

```
greet.go:26: Expected values to be equal:
+Go to hell, Chris!
\ No newline at end of file
```

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

Нам нужно будет обновить нашу спецификацию protocol buffer, чтобы она имела метод `Curse`, а затем перегенерировать наш код.

```protobuf
service Greeter {
  rpc Greet (GreetRequest) returns (GreetReply) {}
  rpc Curse (GreetRequest) returns (GreetReply) {}
}
```

Можно утверждать, что повторное использование типов `GreetRequest` и `GreetReply` является неуместной связью, но мы можем разобраться с этим на этапе рефакторинга. Как я постоянно подчеркиваю, мы просто пытаемся заставить тест пройти, чтобы проверить работу программного обеспечения, *а затем* мы можем сделать его красивым.

Перегенерируйте наш код (внутри `adapters/grpcserver`).

```
protoc --go_out=. --go_opt=paths=source_relative \
    --go-grpc_out=. --go-grpc_opt=paths=source_relative \
    greet.proto
```

### Обновление драйвера

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

```go
func (d *Driver) Curse(name string) (string, error) {
	client, err := d.getClient()
	if err != nil {
		return "", err
	}

	greeting, err := client.Curse(context.Background(), &GreetRequest{
		Name: name,
	})
	if err != nil {
		return "", err
	}

	return greeting.Message, nil
}
```

### Обновление сервера

Наконец, нам нужно добавить метод `Curse` к нашему `Server`.

```go
package grpcserver

import (
	"context"
	"fmt"

	"github.com/quii/go-specs-greet/domain/interactions"
)

type GreetServer struct {
	UnimplementedGreeterServer
}

func (g GreetServer) Curse(ctx context.Context, request *GreetRequest) (*GreetReply, error) {
	return &GreetReply{Message: fmt.Sprintf("Go to hell, %s!", request.Name)}, nil
}

func (g GreetServer) Greet(ctx context.Context, request *GreetRequest) (*GreetReply, error) {
	return &GreetReply{Message: interactions.Greet(request.Name)}, nil
}
```

Тесты теперь должны пройти.

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

Попробуйте сделать это самостоятельно.

* Извлеките "доменную логику" `Curse` из grpc-сервера, как мы это сделали для `Greet`. Используйте спецификацию в качестве модульного теста для вашей доменной логики.
* Используйте разные типы в protobuf, чтобы обеспечить несвязанность типов сообщений для `Greet` и `Curse`.

## Реализация `Curse` для HTTP-сервера

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

* Добавьте спецификацию к существующему приемочному тесту для HTTP-сервера.
* Обновите ваш `Driver`.
* Добавьте новую конечную точку к серверу и повторно используйте доменный код для реализации функциональности. Возможно, вы захотите использовать `http.NewServeMux` для маршрутизации к отдельным конечным точкам.

Помните, что нужно работать небольшими шагами, часто коммитить и запускать тесты. Если вы застрянете, [вы можете найти мою реализацию на GitHub](https://github.com/quii/go-specs-greet).

## Улучшение обеих систем путем обновления доменной логики с помощью модульного теста

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

Добавьте модульный тест к нашей функции `Greet`, чтобы установить значение `name` по умолчанию на `World`, если оно пусто. Вы должны увидеть, как это просто, и тогда бизнес-правила будут отражены в обоих приложениях "бесплатно".

## Завершение

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

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

Вы можете представить разговор с заинтересованной стороной, которая хочет каким-либо образом расширить систему, над которой вы работаете. Зафиксируйте это в доменно-ориентированной, независимой от реализации форме в спецификации и используйте ее как путеводную звезду в своих усилиях. Мы с Рией описываем использование методов BDD, таких как "Примерное картирование" (Example Mapping), [в нашей лекции на GopherconUK](https://www.youtube.com/watch?v=ZMWJCk_0WrY), чтобы помочь вам глубже понять существенную сложность и позволить вам писать более подробные и значимые спецификации.

Разделение существенной и случайной сложности сделает вашу работу менее хаотичной и более структурированной и целенаправленной; это обеспечит отказоустойчивость ваших приемочных тестов и поможет им стать менее обременительными в обслуживании.

Дэйв Фарли дает отличный совет:

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

Спецификации затем должны служить документацией. Они должны четко определять, как должна вести себя система. Эта идея является принципом, лежащим в основе таких инструментов, как [Cucumber](https://cucumber.io), который предлагает вам DSL для фиксации поведения в виде кода, а затем вы преобразуете этот DSL в системные вызовы, как мы это сделали здесь.

### Что было рассмотрено

* Написание абстрактных спецификаций позволяет выразить существенную сложность решаемой проблемы и устранить случайную сложность. Это позволяет повторно использовать спецификации в разных контекстах.
* Как использовать [Testcontainers](https://golang.testcontainers.org) для управления жизненным циклом вашей системы для AT. Это позволяет вам тщательно тестировать образ, который вы собираетесь выпустить, на вашем компьютере, давая вам быструю обратную связь и уверенность.
* Краткое введение в контейнеризацию вашего приложения с помощью Docker.
* gRPC
* Вместо того чтобы гоняться за шаблонными структурами папок, вы можете использовать свой подход к разработке, чтобы естественным образом формировать структуру вашего приложения, исходя из ваших собственных потребностей.

### Дополнительные материалы

* В этом примере наш «DSL» не совсем DSL; мы просто использовали интерфейсы для отделения нашей спецификации от реального мира и позволения чисто выразить доменную логику. По мере роста вашей системы этот уровень абстракции может стать громоздким и неясным. [Ознакомьтесь с «Шаблоном сценария»](https://cucumber.io/blog/bdd/understanding-screenplay-part-1/), если хотите найти больше идей о том, как структурировать свои спецификации.
* Для акцента, [Growing Object-Oriented Software, Guided by Tests](http://www.growing-object-oriented-software.com), — это классика. Она демонстрирует применение этого «лондонского стиля», «нисходящего» подхода к написанию программного обеспечения. Любой, кому понравилась книга «Изучаем Go через тесты», получит большую пользу от прочтения GOOS.
* [В репозитории с примером кода](https://github.com/quii/go-specs-greet) есть больше кода и идей, о которых я здесь не писал, например, многостадийная сборка Docker; возможно, вам захочется это проверить.
  * В частности, *для развлечения* я создал **третью программу**, веб-сайт с несколькими HTML-формами для `Greet` и `Curse`. `Driver` использует отлично выглядящий модуль <https://github.com/go-rod/rod>, который позволяет ему работать с веб-сайтом через браузер, точно так же, как пользователь. Глядя на историю Git, вы можете увидеть, как я сначала не использовал никаких инструментов шаблонизации «просто чтобы заставить его работать». Затем, как только мой приемочный тест прошел, у меня появилась свобода делать это без страха что-либо сломать.


---

# 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-testirovaniya/scaling-acceptance-tests.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.
