> 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/dopolnitelno/anti-patterns.md).

# Антипаттерны

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

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

В этой главе перечислены некоторые антипаттерны TDD и тестирования, а также способы их устранения.

## Полное отсутствие TDD

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

Одна из сильных сторон TDD заключается в том, что он предоставляет формальный процесс для разбиения проблем, понимания того, что вы пытаетесь достичь (красный), выполнения задачи (зелёный), а затем тщательного обдумывания, как это исправить (синий/рефакторинг).

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

## Непонимание ограничений шага рефакторинга

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

> Вам нельзя этого делать! Сначала вы должны написать для этого тест, мы же делаем TDD!

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

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

## Тесты, которые никогда не падают (или вечнозелёные тесты)

Удивительно, как часто это встречается. Вы начинаете отлаживать или изменять некоторые тесты и понимаете: нет сценариев, при которых этот тест может провалиться. Или, по крайней мере, он не провалится так, как тест *должен* был защищать.

Это *практически невозможно* при использовании TDD, если вы следуете **первому шагу**:

> Напишите тест, увидьте его провал

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

## Бесполезные утверждения (assertions)

Работали ли вы когда-нибудь над системой, ломали тест, а затем видели это?

> `false was not equal to true`

Я знаю, что false не равно true. Это не информативное сообщение; оно не говорит мне, что я сломал. Это симптом несоблюдения процесса TDD и плохого сообщения об ошибке.

Возвращаясь к истокам:

> Напишите тест, убедитесь что он упал (и не стесняйтесь сообщения об ошибке)

## Утверждения о несущественных деталях

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

```go
// not this, now your test is tightly coupled to the whole object
if !cmp.Equal(complexObject, want) {
	t.Error("got %+v, want %+v", complexObject, want)
}

// be specific, and loosen the coupling
got := complexObject.fieldYouCareAboutForThisTest
if got != want {
	t.Error("got %q, want %q", got, want)
}
```

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

Это пример недостаточного строгого соблюдения красного этапа.

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

## Множество утверждений в одном сценарии юнит-тестов

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

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

Полезное практическое правило — стремиться к одному утверждению на тест. В Go используйте подтесты (`subtests`), чтобы четко разграничивать утверждения в тех случаях, когда это необходимо. Это также удобный метод для разделения утверждений о поведении и деталях реализации.

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

## Не прислушиваться к своим тестам

[Дэйв Фарли в своем видео "Когда TDD идет не так"](https://www.youtube.com/watch?v=UWtEVKVPBQ0\&feature=youtu.be) отмечает:

> TDD дает вам максимально быструю обратную связь по вашему дизайну

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

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

Я много раз подчеркивал это в книге, и скажу еще раз: **прислушивайтесь к своим тестам**.

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

Вы когда-нибудь видели тест с 20, 50, 100, 200 строками кода настройки, прежде чем в тесте произойдет что-то интересное? Приходилось ли вам потом менять код, возвращаться к этому беспорядку и жалеть, что вы не выбрали другую карьеру?

Какие здесь сигналы? *Прислушайтесь*: сложные тесты `==` сложный код. Почему ваш код сложен? Должен ли он быть таким?

* Когда в ваших тестах много тестовых дублеров (`test doubles`), это означает, что тестируемый вами код имеет много зависимостей, а это значит, что ваш дизайн нуждается в доработке.
* Если ваш тест зависит от настройки различных взаимодействий с моками (`mocks`), это означает, что ваш код осуществляет множество взаимодействий со своими зависимостями. Спросите себя, могут ли эти взаимодействия быть проще.

#### "Протекающие" интерфейсы

Если вы объявили `interface` с множеством методов, это указывает на "протекающую" абстракцию. Подумайте, как вы могли бы определить это взаимодействие с более консолидированным набором методов, в идеале — с одним.

#### Загрязнение интерфейса

Как гласит поговорка Go: *чем больше интерфейс, тем слабее абстракция*. Если вы предоставляете пользователям вашего пакета огромный `interface`, вы вынуждаете их создавать в своих тестах заглушку/мок, которая соответствует всему API, предоставляя реализацию также для методов, которые они не используют (иногда они просто `panic`, чтобы дать понять, что эти методы не должны использоваться). Эта ситуация является антипаттерном, известным как [загрязнение интерфейса](https://rakyll.org/interface-pollution/), и именно по этой причине стандартная библиотека предлагает вам лишь крошечные `интерфейсы`.

Вместо этого вам следует предоставить из вашего пакета простую `структуру` со всеми экспортируемыми релевантными методами, оставляя клиентам вашего API свободу объявлять свои собственные `интерфейсы`, абстрагируясь от подмножества необходимых им методов: например, [go-redis](https://github.com/redis/go-redis) предоставляет `структуру` (`redis.Client`) клиентам API.

В общем, вы должны предоставлять `интерфейс` клиентам только тогда, когда:

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

#### Подумайте о типах используемых тестовых дублеров

* Моки (`mocks`) иногда полезны, но они чрезвычайно мощны и потому легко могут быть использованы неправильно. Попробуйте поставить себе ограничение на использование стабов (`stubs`) вместо них.
* Проверка деталей реализации с помощью спаев (`spies`) иногда полезна, но старайтесь избегать ее. Помните, что детали вашей реализации обычно не важны, и вы не хотите, чтобы ваши тесты были связаны с ними, если это возможно. Стремитесь связывать ваши тесты с **полезным поведением, а не со случайными деталями**.
* [Прочитайте мои посты о наименовании тестовых дублеров](https://quii.dev/Start_naming_your_test_doubles_correctly), если таксономия тестовых дублеров (`test doubles`) немного неясна.

#### Консолидация зависимостей

Вот код для `http.HandlerFunc` для обработки новых регистраций пользователей на веб-сайте.

```go
type User struct {
	// Some user fields
}

type UserStore interface {
	CheckEmailExists(email string) (bool, error)
	StoreUser(newUser User) error
}

type Emailer interface {
	SendEmail(to User, body string, subject string) error
}

func NewRegistrationHandler(userStore UserStore, emailer Emailer) http.HandlerFunc {
	return func(writer http.ResponseWriter, request *http.Request) {
		// extract out the user from the request body (handle error)
		// check user exists (handle duplicates, errors)
		// store user (handle errors)
		// compose and send confirmation email (handle error)
		// if we got this far, return 2xx response
	}
}
```

На первый взгляд, разумно сказать, что дизайн не так уж плох. У него всего 2 зависимости!

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

* Разобрать тело запроса в `User` :white\_check\_mark:
* Использовать `UserStore` для проверки существования пользователя :question:
* Использовать `UserStore` для сохранения пользователя :question:
* Сформировать электронное письмо :question:
* Использовать `Emailer` для отправки электронного письма :question:
* Вернуть соответствующий `http` ответ, в зависимости от успеха, ошибок и т.д. :white\_check\_mark:

Чтобы протестировать этот код, вам придется написать множество тестов с различными настройками тестовых дублеров (`test double setups`), спаев (`spies`) и т.д.

* Что, если требования расширятся? Переводы для электронных писем? Отправка SMS-подтверждения тоже? Кажется ли вам разумным, что вам придется менять `HTTP` обработчик, чтобы учесть это изменение?
* Кажется ли вам правильным, что важное правило "мы должны отправить электронное письмо" находится внутри `HTTP` обработчика?
  * Почему вы должны проходить через церемонию создания `HTTP` запросов и чтения ответов, чтобы проверить это правило?

**Прислушивайтесь к своим тестам**. Написание тестов для этого кода в стиле TDD должно быстро вызвать у вас дискомфорт (или, по крайней мере, раздражить ленивого разработчика внутри вас). Если это кажется болезненным, остановитесь и подумайте.

Что, если бы дизайн был таким?

```go
type UserService interface {
	Register(newUser User) error
}

func NewRegistrationHandler(userService UserService) http.HandlerFunc {
	return func(writer http.ResponseWriter, request *http.Request) {
		// parse user
		// register user
		// check error, send response
	}
}
```

* Обработчик легко тестировать ✅
* Изменения в правилах регистрации изолированы от `HTTP`, поэтому их также проще тестировать ✅

## Нарушение инкапсуляции

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

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

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

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

## Сложные табличные тесты

Табличные тесты (`table tests`) — отличный способ проработать ряд различных сценариев, когда настройка теста одинакова, и вы хотите варьировать только входные данные.

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

```go
cases := []struct {
	X                int
	Y                int
	Z                int
	err              error
	IsFullMoon       bool
	IsLeapYear       bool
	AtWarWithEurasia bool
}{}
```

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

При написании программного обеспечения следует помнить:

> [Простое — это не легкое](https://www.infoq.com/presentations/Simple-Made-Easy/)

«Простое» добавление поля в таблицу может быть легким, но это может сделать вещи далекими от простоты.

## Итог

Большинство проблем с юнит-тестами обычно можно свести к:

* Разработчики не следуют процессу TDD
* Плохой дизайн

Итак, изучайте хороший дизайн программного обеспечения!

Хорошая новость заключается в том, что TDD может помочь вам *улучшить ваши навыки дизайна*, потому что, как было сказано в начале:

**Основная цель TDD — предоставить обратную связь по вашему дизайну.** В миллионный раз говорю: прислушивайтесь к своим тестам, они отражают ваш дизайн обратно вам.

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


---

# 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/dopolnitelno/anti-patterns.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.
