> 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/why.md).

# Зачем нужны модульные тесты и как заставить их работать на вас

[Здесь ссылка на видео, где я рассказываю об этой теме](https://www.youtube.com/watch?v=Kwtit8ZEK7U)

Если вы не любите видео, вот текстовая версия.

## Программное обеспечение

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

Так почему же мы так плохо справляемся с этим? О скольких проектах вы слышали, которые полностью провалились? Или стали "наследием" (legacy) и должны быть полностью переписаны (и переписывания часто тоже терпят неудачу!)

Как вообще программная система "терпит неудачу"? Разве её нельзя просто менять, пока она не станет правильной? Это то, что нам обещали!

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

* По сравнению с моей предыдущей жизнью в Scala, где [я описывал, как в ней достаточно возможностей, чтобы выстрелить себе в ногу](http://www.quii.dev/Scala_-_Just_enough_rope_to_hang_yourself), в Go всего 25 ключевых слов, и *много* систем можно построить из стандартной библиотеки и нескольких других небольших библиотек. Надежда состоит в том, что с Go вы можете написать код, вернуться к нему через 6 месяцев, и он всё ещё будет иметь смысл.
* Инструменты для тестирования, бенчмаркинга, линтинга и развёртывания являются первоклассными по сравнению с большинством альтернатив.
* Стандартная библиотека великолепна.
* Очень высокая скорость компиляции для коротких циклов обратной связи.
* Обещание обратной совместимости Go. Похоже, Go получит дженерики и другие функции в будущем, но разработчики пообещали, что даже код Go, написанный 5 лет назад, всё равно будет собираться. Я буквально потратил недели на обновление проекта со Scala 2.8 до 2.10.

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

В 1974 году умный инженер-программист по имени [Мэнни Леман](https://en.wikipedia.org/wiki/Manny_Lehman_%28computer_scientist%29) написал [законы эволюции ПО Лемана](https://en.wikipedia.org/wiki/Lehman%27s_laws_of_software_evolution).

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

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

## Закон непрерывных изменений

> Любая программная система, используемая в реальном мире, должна меняться или становиться всё менее полезной в своей среде.

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

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

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

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

Леман был в ударе в 70-х, потому что он дал нам ещё один закон для размышления.

## Закон возрастающей сложности

> По мере эволюции системы её сложность возрастает, если не предпринимаются действия по её снижению.

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

Мы **должны** продолжать управлять сложностью системы по мере изменения наших знаний о предметной области.

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

Существует *множество* аспектов инженерии программного обеспечения, которые поддерживают его податливость, такие как:

* Расширение прав и возможностей разработчиков
* В целом "хороший" код. Разумное разделение обязанностей и т.д.
* Коммуникативные навыки
* Архитектура
* Наблюдаемость
* Развёртываемость
* Автоматизированные тесты
* Циклы обратной связи

Я собираюсь сосредоточиться на рефакторинге. Это фраза, которую часто произносят: "нам нужно это отрефакторить" — говорят разработчику в первый день его программирования, не задумываясь.

Откуда взялась эта фраза? Чем рефакторинг отличается от простого написания кода?

Я знаю, что я, как и многие другие, *думал*, что мы делали рефакторинг, но мы ошибались.

[Мартин Фаулер описывает, как люди ошибаются](https://martinfowler.com/bliki/RefactoringMalapropism.html)

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

Так что же это такое?

### Факторизация

Изучая математику в школе, вы, вероятно, узнали о факторизации. Вот очень простой пример:

Вычислите `1/2 + 1/4`

Для этого вы *факторизуете* знаменатели, превращая выражение в

`2/4 + 1/4`, что затем можно превратить в `3/4`.

Из этого мы можем извлечь несколько важных уроков. Когда мы *факторизуем выражение*, мы **не изменяем смысл выражения**. Оба они равны `3/4`, но мы облегчили работу с ними; изменив `1/2` на `2/4`, оно легче вписывается в нашу "область".

Когда вы рефакторите свой код, вы пытаетесь найти способы сделать его более понятным и "вписать" в ваше текущее понимание того, что должна делать система. Важно то, что **вы не должны менять поведение**.

#### Пример в Go

Вот функция, которая приветствует `name` на определённом `language`:

```go
func Hello(name, language string) string {

  if language == "es" {
     return "Hola, " + name
  }

  if language == "fr" {
     return "Bonjour, " + name
  }
  
  // imagine dozens more languages

  return "Hello, " + name
}
```

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

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

var greetings = map[string]string {
  "es": "Hola",
  "fr": "Bonjour",
  //etc..
}

func greeting(language string) string {
  greeting, exists := greetings[language]
  
  if exists {
     return greeting
  }
  
  return "Hello"
}
```

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

При рефакторинге вы можете делать всё, что угодно: добавлять интерфейсы, новые типы, функции, методы и т.д. Единственное правило — вы не меняете поведение.

### При рефакторинге кода вы не должны менять поведение

Это очень важно. Если вы одновременно меняете поведение, вы делаете *две* вещи сразу. Как инженеры-программисты, мы учимся разбивать системы на разные файлы/пакеты/функции/и т.д., потому что знаем, что пытаться понять большой объём информации сложно.

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

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

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

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

* Уверенность, что вы можете изменять код, не беспокоясь об изменении поведения.
* Документацию для людей о том, как система должна себя вести.
* Гораздо более быструю и надёжную обратную связь, чем ручное тестирование.

#### Пример в Go

Юнит-тест для нашей функции `Hello` может выглядеть так:

```go
func TestHello(t *testing.T) {
  got := Hello("Chris", "es") // Fixed 'es' to be a string literal for Go
  want := "Hola, Chris"

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

В командной строке я могу запустить `go test` и получить немедленную обратную связь о том, изменили ли мои усилия по рефакторингу поведение. На практике лучше научиться пользоваться "волшебной кнопкой" для запуска тестов в вашем редакторе/IDE.

Вы хотите прийти к состоянию, когда вы делаете:

* Небольшой рефакторинг
* Запускаете тесты
* Повторяете

Всё это в очень коротком цикле обратной связи, чтобы вы не заходили в кроличьи норы и не совершали ошибок.

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

## Если юнит-тесты так хороши, почему иногда возникает сопротивление их написанию?

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

С другой стороны, есть люди, описывающие случаи, когда юнит-тесты фактически *препятствуют* рефакторингу.

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

Это противоположность тому, что нам обещали!

### Почему это происходит?

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

![Два прямоугольных треугольника образуют квадрат](https://i.imgur.com/ela7SVf.jpg)

Мы пишем юнит-тесты для нашего квадрата, чтобы убедиться, что стороны равны, а затем пишем тесты для наших треугольников. Мы хотим убедиться, что наши треугольники отображаются правильно, поэтому мы утверждаем, что сумма углов составляет 180 градусов, возможно, проверяем, что их 2, и так далее. Покрытие тестами очень важно, и написание этих тестов довольно просто, так почему бы и нет?

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

![Два прямоугольника образуют квадрат](https://i.imgur.com/1G6rYqD.jpg)

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

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

## Отдавайте предпочтение тестированию поведения, а не деталей реализации

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

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

Если я говорю только о тестировании поведения, разве мы не должны писать только системные тесты / тесты чёрного ящика? Такие тесты имеют большую ценность с точки зрения проверки ключевых пользовательских сценариев, но они, как правило, дороги в написании и медленны в выполнении. По этой причине они не очень полезны для *рефакторинга*, потому что цикл обратной связи медленный. Кроме того, тесты чёрного ящика не слишком помогают с поиском первопричин по сравнению с юнит-тестами.

Так какой же *правильный* уровень абстракции?

## Написание эффективных юнит-тестов — это проблема проектирования

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

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

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

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

### Это юнит-тесты?

**ДА**. Юнит-тесты направлены на "единицы", как я описал. Они *никогда* не были исключительно о тестировании отдельного класса/функции/чего-либо ещё.

## Объединение этих концепций

Мы рассмотрели:

* Рефакторинг
* Юнит-тесты
* Проектирование единиц (Units)

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

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

* Даёт нам сигналы о наших юнит-тестах. Если нам приходится выполнять ручные проверки, нам нужно больше тестов. Если тесты ошибочно падают, то наши тесты находятся на неправильном уровне абстракции (или не имеют ценности и должны быть удалены).
* Помогает нам справляться со сложностями внутри наших единиц и между ними.

### Юнит-тесты

* Обеспечивают страховку для рефакторинга.
* Проверяют и документируют поведение наших единиц.

### (Хорошо спроектированные) единицы

* Легко писать *осмысленные* юнит-тесты.
* Легко рефакторить.

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

## Почему разработка через тестирование (TDD)

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

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

Я говорю "плохие старые времена", но это до сих пор происходит!

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

TDD учитывает законы, о которых говорит Леман, и другие уроки, извлечённые из истории, поощряя методологию постоянного рефакторинга и итеративной поставки.

### Малые шаги

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

По мере того, как вы станете опытнее, такой способ работы станет естественным и быстрым.

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

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

## В заключение

* Сила программного обеспечения в том, что мы можем его менять. *Большинству* программного обеспечения со временем потребуются изменения непредсказуемым образом; но не пытайтесь перепроектировать, потому что слишком сложно предсказать будущее.
* Вместо этого мы должны сделать так, чтобы наше ПО оставалось податливым. Чтобы изменить ПО, мы должны рефакторить его по мере эволюции, иначе оно превратится в беспорядок.
* Хороший набор тестов может помочь вам рефакторить быстрее и с меньшим стрессом.
* Написание хороших юнит-тестов — это проблема проектирования, поэтому думайте о структурировании вашего кода таким образом, чтобы у вас были осмысленные единицы, которые вы можете интегрировать вместе, как кирпичики Lego.
* 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/why.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.
