<commit_before>Классическое модульное тестирование (unit-тестирование) предполагает возможность осуществления рефакторинга и разбиения большой программы на маленькие блоки, удобные для тестов. Можно изначально разрабатывать программу, удовлетворяющую тестам (TDD - test driven development).

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

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

Под катом описаны несколько подходов к тестированию с разной степенью сложности (вовлеченности) и разной степенью покрытия.

<cut />

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

### Тестовый стенд (test bench)

Прежде всего, так как тестируется всего одна функция, код вызова которой всегда одинаков, то у нас нет необходимости создавать отдельные unit test'ы. Достаточно в цикле передавать исходные данные (`input`) и проверять результаты (`expectedOutput`). Чтобы в случае обнаружения ошибки можно было идентифицировать проблемный набор тестовых данных, все тестовые данные надо снабдить меткой (`label`). Таким образом, один набор тестовых данных можно представить в виде тройки:

```scala
case class TestCase[A, B](label: String, input: A, expectedOutput: B)
```

Результат одного прогона можно представить в виде `TestCaseResult`:
```scala
case class TestCaseResult[A, B](testCase: TestCase[A, B], actualOutput: Try[B])
```
(Результат запуска мы представляем с помощью `Try`, чтобы захватить возможные исключения.)

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

```scala
def runTestCases[A, B](cases: Seq[TestCase[A, B])(f: A => B): Seq[TestCaseResult[A, B]] = 
  cases
    .map{ testCase => 
      TestCaseResult(testCase, 
        Try{ f(testCase.input) }
      ) 
    }
    .filter(r => r.actualOutput != Success(r.testCase.expectedOutput))
```

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

Для удобства можно отформатировать результаты тестирования
```scala
def report(results: Seq[TestCaseResult[_, _]]): String = 
  s"Failed ${results.length}:\n" + 
    results
      .map(r => r.testCase.label + ": expected " + r.testCase.expectedOutput + ", but got " + r.actualOutput)
      .mkString("\n")
```

и выводить отчёт только в случае ошибок:
```scala
val testCases = Seq(
  TestCase("1", 0, 0)
)

test("all test cases"){
  val testBench = runTestCases(testCases) _
  val results = testBench(f)
  assert(results.isEmpty, report(results))
}
```

### Подготовка входных данных

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

```scala
val baseline = MyObject(...)
val testCases = Seq(
  TestCase("baseline", baseline, ???),
  TestCase("baseline + (field1 = 123)", baseline.copy(field1 = "123"), ???)
)
```

При вложенных структурах данных большим подспорьем являются линзы, например, из библиотеки [Monocle](http://julien-truffaut.github.io/Monocle/):

```scala
val testObject1 = (field1 composeLens field2).set("123")(baseline)
// val testObject1 = baseline.copy(field1 = baseline.field1.copy(field2 = "123"))
```

### Использование DSL для представления изменений

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

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

Вторым способом, который позволяет манипулировать перечнем изменений, является создание встроенного специализированного языка (DSL), описывающего изменения. Такой язык должен быть достаточно выразительным, чтобы описывать все интересующие нас изменения. Результатом разбора языка изменений будет модель, содержащая перечень изменений. Потребуется также интерпретатор, который сможет применить изменения, описываемые моделью, к переданному объекту.

<spoiler title="Подробности языка изменений">
Язык изменений представляет собой довольно сложную конструкцию, включающую несколько компонентов, которые также, в свою очередь, нетривиальны.
1. Модель структуры данных. 
2. Модель изменений.
3. Собственно Embedded(?) DSL - вспомогательные конструкции, extension-методы, для удобного конструирования изменений.
4. Интерпретатор изменений, позволяющий фактически "модифицировать" объект (на самом деле, естественно, создать изменённую копию).

Приведём пример
```scala
val target: Entity[Target]
val updateField1 = target \ field1 := "123"
val updateField1 = Set(PropertyAccess(target, Property(field1, typeTag[String])), LiftedString("123"))
```
То есть с помощью extension-методов `\` и `:=` формируется объекты `PropertyAccess`, `Set` из ранее созданных объектов `target` и `field1`. Также за счёт (опасных) implicit-конвертаций строка "123" упаковывается в `LiftedString`.

В качестве модели данных может быть использована типизированная онтология (см. https://habr.com/post/229035/ и https://habr.com/post/222553/).

Для представления изменений необходим набор классов наподобие `Set`:
- `Modify` - применение функции
- `Changes` - применение нескольких изменений последовательно
- `ForEach` - применение изменений к коллекции элементов
- и т.д.

Интерпретатор представляет собой обычный рекурсивный вычислитель выражений, основанный на PatternMatching'е. Для непосредственного оперирования свойствами объектов необходимо отобразить свойства, описанные в онтологии и используемые в модели изменений, на линзы, содержащие getter и setter для каждого свойства.
</spoiler>

Такой подход в целом работает, и действительно позволяет описывать изменения один раз, однако постепенно появляется потребность представлять сложные изменения и модель изменений существенно усложняется. Например, если необходимо изменить какое-то свойство с использованием другого свойства того же объекта, то возникает необходимость в объявлении имён и представлении гамма-контекста, если изменение свойства нетривиально, то потребуется представлять функции.

### Тестирование ветвей

Тестируемый код может быть линейным, и тогда нам по большому счёту достаточно одного набора тестовых данных, чтобы понять, работает ли он. В случае наличия ветвления (`if-then-else`), необходимо запускать белый ящик как минимум дважды с разными входными данными, чтобы были исполнены обе ветки. Количество наборов входных данных, по-видимому, численно равно цикломатической сложности кода с ветвлениями.

Как сформировать все наборы входных данных? Так как мы имеем дело с белым ящиком, то мы можем вычленить условия ветвления и дважды модифицировать входной объект так, чтобы в одном случае выполнялась одна ветвь, в другом случае - другая. Рассмотрим пример:
```scala
if (object.field1 == "123") A else B
```
Имея такое условие, мы можем сформировать два тестовых случая:
```scala
val testCase1 = TestCase("A", field1.set("123")(baseline), /* result of A */)
val testCase2 = TestCase("B", field1.set(/* не "123", а, например, */"123" + "1">)(baseline), /*result of B*/)
```
*(В случае, если один из тестовых сценариев невозможно создать, то тем самым можно считать, что обнаружен мертвый код и условие вместе с соответствующей веткой можно спокойно удалить.)*

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

<spoiler title="DSL для формирования всех комбинаций изменений">
Во-первых, чтобы изменения накапливались, должна быть возможность их композиции. Для этого можно либо воспользоваться вышеприведённым DSL для моделирования изменений, и тогда достаточно список изменений завернуть в `Changes`, либо представить одно изменение в виде функции модификации `T => T`, тогда цепочка изменений будет представлять собой просто композицию функций:
```scala
val change1: T => T = field1.set("123")(_)
val change2: T => T = field2.set("456")
val changes = change1 compose change2

// или для списка изменений:
val rawChanges: Seq[T => T] = Seq(change1, change2)
val allChanges: T => T = rawChanges.foldLeft(identity)(_ compose _)
```

Чтобы компактно записать все изменения, соответствующие всем возможным ветвлениям, можно использовать DSL, моделирующий структуру тестируемого белого ящика:
```scala
val tests: Seq[(String, T=>T)] =
           IF("field1 == '123'")  // название условия, которое мы моделируем
           THEN( field1.set("123"))( // или target \ field1 := "123"
             IF("field2 == '456')
             THEN(field2.set("456"))(TERMINATE)
             ELSE(field2.set("456" + "1"))(TERMINATE)
           )
           ELSE( field1.set("123" + "1") )(TERMINATE)
```
(причем в коллекции `tests` - агрегированные изменения, соответствующие всем возможным комбинациям ветвей. `String` будет содержать все названия условий и все изменения, из которых сформирована агрегированная функция изменений. )

Чтобы получить изменённые объекты, надо все функции изменений применить к baseline:
```scala
val tests2: Seq[(String, T)] = tests.map(_.map_2(_(baseline)))
```

Также DSL может производить изменения сразу над baseline-объектом. В этом случае будет производиться меньше мусора.
</spoiler>

### Автоматическое формирование тестовых данных

Так как мы имеем дело с белым ящиком, то фактически можем видеть все ветвления. Мы можем построить модель логики, содержащейся в белом ящике, чтобы использовать её для генерации тестовых данных. В случае, если тестируемый код написан на Scala, можно, например, использовать [scalameta](https://scalameta.org/), для чтения кода, с последующем преобразованием в модель логики. Далее будем предполагать, что тестируемый код реализован с использованием ограниченного подмножества языка, либо на другом языке, который изначально ограничен. Это позволяет сосредоточиться на тех аспектах языка, которые представляют интерес.

Рассмотрим пример кода, содержащего единственное ветвление:
```scala
if(object.field1 == "123") A else B
```
Условие разбивает множество значений поля `field1` на два класса: `== "123"` и `!= "123"`. Нам достаточно взять по одному примеру из этих двух классов, чтобы покрыть обе ветви. Для первого класса пример единственный (взять случайный объект, но изменить поле `field1` на `"123"`), а для второго класса примеров множество. Для генерации какого-то примера второго класса можно использовать генератор произвольного объекта и отбросить неподходящий объект, с полем равным `field1 == "123"`. В качестве генераторов данных вполне подходит [механизм генераторов `Arbitrary` и `Gen` из библиотеки ScalaCheck](https://github.com/rickynils/scalacheck/blob/master/doc/UserGuide.md#generators).

Подобным образом можно генерировать и другие ограничения, порождаемые простыми условными операторами с константами (больше/меньше константы, входит во множество, начинается с константы). Сложно генерировать примеры для ограничений, заданных функциями, особенно, если функция трудно обращаема. Если используется хэш-функция, автоматически генерировать пример, дающий требуемое значение, по-видимому, не получится. В таком случае, можно попробовать добавить в объект дополнительное поле, представляющее результат вычисления функции, и обновлять это поле, невзирая на нарушение функциональной связи:
```scala
if(sha(object.field1)=="a9403...") ...
// ==> 
if(object.sha_field1 == "a9403...") ...
```

Если ветвления в коде опираются на разные поля объекта, то достаточно записать все модели подмножеств для каждого тестируемого поля, а затем, используя подходящие генераторы, сгенерировать новые значения для всех полей. Если же последующие условия уточняют множество значений поля, то необходимо брать пересечение подмножеств. (Например, первое условие, `x>0`, а следующее  - `x<=1`. Получится три подмножества, примеры из которых надо будет сгенерировать.) Если окажется, что одно из подмножеств пустое, то соответствующая ветка является "мертвым кодом".

Рассмотрим случай, когда условие ветвления основано на двух полях объекта, также связанных условиями:
```
if(x > 0)
  if(y > 0)
   if (y > x)
```
Если условие является достаточно "свободным", то можно просто сгенерировать примеры значений полей, проверить выполнение условий, и отбросить неподходящие объекты.
Если условие "жёсткое", функциональное (`y == x + 1`), то можно построить функцию, вычисляющую значение второго поля на основе сгенерированного первого.
Если условие "полужесткое" (`y> x + 1 && y < x + 2`), то можно построить зависимый генератор, диапазон значений которого зависит от сгенерированного значения первого поля.
<commit_msg>Add test data, data gen, meaning of testing (2)<commit_after>Классическое модульное тестирование (unit-тестирование) предполагает возможность осуществления рефакторинга и разбиения большой программы на маленькие блоки, удобные для тестов. Можно изначально разрабатывать программу, удовлетворяющую тестам (TDD - test driven development).

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

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

Под катом описаны несколько подходов к тестированию с разной степенью сложности (вовлеченности) и разной степенью покрытия.

<cut />

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

### Тестовый стенд (test bench)

Прежде всего, так как тестируется всего одна функция, код вызова которой всегда одинаков, то у нас нет необходимости создавать отдельные unit test'ы. Достаточно в цикле передавать исходные данные (`input`) и проверять результаты (`expectedOutput`). Чтобы в случае обнаружения ошибки можно было идентифицировать проблемный набор тестовых данных, все тестовые данные надо снабдить меткой (`label`). Таким образом, один набор тестовых данных можно представить в виде тройки:

```scala
case class TestCase[A, B](label: String, input: A, expectedOutput: B)
```

Результат одного прогона можно представить в виде `TestCaseResult`:
```scala
case class TestCaseResult[A, B](testCase: TestCase[A, B], actualOutput: Try[B])
```
(Результат запуска мы представляем с помощью `Try`, чтобы захватить возможные исключения.)

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

```scala
def runTestCases[A, B](cases: Seq[TestCase[A, B])(f: A => B): Seq[TestCaseResult[A, B]] = 
  cases
    .map{ testCase => 
      TestCaseResult(testCase, 
        Try{ f(testCase.input) }
      ) 
    }
    .filter(r => r.actualOutput != Success(r.testCase.expectedOutput))
```

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

Для удобства можно отформатировать результаты тестирования
```scala
def report(results: Seq[TestCaseResult[_, _]]): String = 
  s"Failed ${results.length}:\n" + 
    results
      .map(r => r.testCase.label + ": expected " + r.testCase.expectedOutput + ", but got " + r.actualOutput)
      .mkString("\n")
```

и выводить отчёт только в случае ошибок:
```scala
val testCases = Seq(
  TestCase("1", 0, 0)
)

test("all test cases"){
  val testBench = runTestCases(testCases) _
  val results = testBench(f)
  assert(results.isEmpty, report(results))
}
```

### Подготовка входных данных

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

```scala
val baseline = MyObject(...)
val testCases = Seq(
  TestCase("baseline", baseline, ???),
  TestCase("baseline + (field1 = 123)", baseline.copy(field1 = "123"), ???)
)
```

При вложенных структурах данных большим подспорьем являются линзы, например, из библиотеки [Monocle](http://julien-truffaut.github.io/Monocle/):

```scala
val testObject1 = (field1 composeLens field2).set("123")(baseline)
// val testObject1 = baseline.copy(field1 = baseline.field1.copy(field2 = "123"))
```

### Использование DSL для представления изменений

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

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

Вторым способом, который позволяет манипулировать перечнем изменений, является создание встроенного специализированного языка (DSL), описывающего изменения. Такой язык должен быть достаточно выразительным, чтобы описывать все интересующие нас изменения. Результатом разбора языка изменений будет модель, содержащая перечень изменений. Потребуется также интерпретатор, который сможет применить изменения, описываемые моделью, к переданному объекту.

<spoiler title="Подробности языка изменений">
Язык изменений представляет собой довольно сложную конструкцию, включающую несколько компонентов, которые также, в свою очередь, нетривиальны.
1. Модель структуры данных. 
2. Модель изменений.
3. Собственно Embedded(?) DSL - вспомогательные конструкции, extension-методы, для удобного конструирования изменений.
4. Интерпретатор изменений, позволяющий фактически "модифицировать" объект (на самом деле, естественно, создать изменённую копию).

Приведём пример
```scala
val target: Entity[Target]
val updateField1 = target \ field1 := "123"
val updateField1 = Set(PropertyAccess(target, Property(field1, typeTag[String])), LiftedString("123"))
```
То есть с помощью extension-методов `\` и `:=` формируется объекты `PropertyAccess`, `Set` из ранее созданных объектов `target` и `field1`. Также за счёт (опасных) implicit-конвертаций строка "123" упаковывается в `LiftedString`.

В качестве модели данных может быть использована типизированная онтология (см. https://habr.com/post/229035/ и https://habr.com/post/222553/).

Для представления изменений необходим набор классов наподобие `Set`:
- `Modify` - применение функции
- `Changes` - применение нескольких изменений последовательно
- `ForEach` - применение изменений к коллекции элементов
- и т.д.

Интерпретатор представляет собой обычный рекурсивный вычислитель выражений, основанный на PatternMatching'е. Для непосредственного оперирования свойствами объектов необходимо отобразить свойства, описанные в онтологии и используемые в модели изменений, на линзы, содержащие getter и setter для каждого свойства.
</spoiler>

Такой подход в целом работает, и действительно позволяет описывать изменения один раз, однако постепенно появляется потребность представлять сложные изменения и модель изменений существенно усложняется. Например, если необходимо изменить какое-то свойство с использованием другого свойства того же объекта, то возникает необходимость в объявлении имён и представлении гамма-контекста, если изменение свойства нетривиально, то потребуется представлять функции.

### Тестирование ветвей

Тестируемый код может быть линейным, и тогда нам по большому счёту достаточно одного набора тестовых данных, чтобы понять, работает ли он. В случае наличия ветвления (`if-then-else`), необходимо запускать белый ящик как минимум дважды с разными входными данными, чтобы были исполнены обе ветки. Количество наборов входных данных, по-видимому, численно равно цикломатической сложности кода с ветвлениями.

Как сформировать все наборы входных данных? Так как мы имеем дело с белым ящиком, то мы можем вычленить условия ветвления и дважды модифицировать входной объект так, чтобы в одном случае выполнялась одна ветвь, в другом случае - другая. Рассмотрим пример:
```scala
if (object.field1 == "123") A else B
```
Имея такое условие, мы можем сформировать два тестовых случая:
```scala
val testCase1 = TestCase("A", field1.set("123")(baseline), /* result of A */)
val testCase2 = TestCase("B", field1.set(/* не "123", а, например, */"123" + "1">)(baseline), /*result of B*/)
```
*(В случае, если один из тестовых сценариев невозможно создать, то тем самым можно считать, что обнаружен мертвый код и условие вместе с соответствующей веткой можно спокойно удалить.)*

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

<spoiler title="DSL для формирования всех комбинаций изменений">
Во-первых, чтобы изменения накапливались, должна быть возможность их композиции. Для этого можно либо воспользоваться вышеприведённым DSL для моделирования изменений, и тогда достаточно список изменений завернуть в `Changes`, либо представить одно изменение в виде функции модификации `T => T`, тогда цепочка изменений будет представлять собой просто композицию функций:
```scala
val change1: T => T = field1.set("123")(_)
val change2: T => T = field2.set("456")
val changes = change1 compose change2

// или для списка изменений:
val rawChanges: Seq[T => T] = Seq(change1, change2)
val allChanges: T => T = rawChanges.foldLeft(identity)(_ compose _)
```

Чтобы компактно записать все изменения, соответствующие всем возможным ветвлениям, можно использовать DSL, моделирующий структуру тестируемого белого ящика:
```scala
val tests: Seq[(String, T=>T)] =
           IF("field1 == '123'")  // название условия, которое мы моделируем
           THEN( field1.set("123"))( // или target \ field1 := "123"
             IF("field2 == '456')
             THEN(field2.set("456"))(TERMINATE)
             ELSE(field2.set("456" + "1"))(TERMINATE)
           )
           ELSE( field1.set("123" + "1") )(TERMINATE)
```
(причем в коллекции `tests` - агрегированные изменения, соответствующие всем возможным комбинациям ветвей. `String` будет содержать все названия условий и все изменения, из которых сформирована агрегированная функция изменений. )

Чтобы получить изменённые объекты, надо все функции изменений применить к baseline:
```scala
val tests2: Seq[(String, T)] = tests.map(_.map_2(_(baseline)))
```

Также DSL может производить изменения сразу над baseline-объектом. В этом случае будет производиться меньше мусора.
</spoiler>

### Автоматическое формирование тестовых данных

Так как мы имеем дело с белым ящиком, то фактически можем видеть все ветвления. Мы можем построить модель логики, содержащейся в белом ящике, чтобы использовать её для генерации тестовых данных. В случае, если тестируемый код написан на Scala, можно, например, использовать [scalameta](https://scalameta.org/) для чтения кода, с последующем преобразованием в модель логики. Далее будем предполагать, что тестируемый код реализован с использованием ограниченного подмножества языка, либо на другом языке, который изначально ограничен. Это позволяет сосредоточиться на тех аспектах языка, которые представляют интерес.

Рассмотрим пример кода, содержащего единственное ветвление:
```scala
if(object.field1 == "123") A else B
```
Условие разбивает множество значений поля `field1` на два класса: `== "123"` и `!= "123"`. Нам достаточно взять по одному примеру из этих двух классов, чтобы покрыть обе ветви. Для первого класса пример единственный (взять случайный объект, но изменить поле `field1` на `"123"`), а для второго класса примеров множество. Для генерации какого-то примера второго класса можно использовать генератор произвольного объекта и отбросить неподходящий объект, с полем равным `field1 == "123"`. В качестве генераторов данных вполне подходит [механизм генераторов `Arbitrary` и `Gen` из библиотеки ScalaCheck](https://github.com/rickynils/scalacheck/blob/master/doc/UserGuide.md#generators).

Подобным образом можно генерировать и другие ограничения, порождаемые простыми условными операторами с константами (больше/меньше константы, входит во множество, начинается с константы). Сложно генерировать примеры для ограничений, заданных функциями, особенно, если функция трудно обращаема. Если используется хэш-функция, автоматически генерировать пример, дающий требуемое значение, по-видимому, не получится. В таком случае, можно попробовать добавить в объект дополнительное поле, представляющее результат вычисления функции, и обновлять это поле, невзирая на нарушение функциональной связи:
```scala
if(sha(object.field1)=="a9403...") ...
// ==> 
if(object.sha_field1 == "a9403...") ...
```

Если ветвления в коде опираются на разные поля объекта, то достаточно записать все модели подмножеств для каждого тестируемого поля, а затем, используя подходящие генераторы, сгенерировать новые значения для всех полей. Если же последующие условия уточняют множество значений поля, то необходимо брать пересечение подмножеств. (Например, первое условие, `x>0`, а следующее  - `x<=1`. Получится три подмножества, примеры из которых надо будет сгенерировать.) Если окажется, что одно из подмножеств пустое, то соответствующая ветка является "мертвым кодом".

Рассмотрим случай, когда условие ветвления основано на двух полях объекта, также связанных условиями:
```scala
if(x > 0)
  if(y > 0)
   if (y > x)
```
Если условие является достаточно "свободным", то можно просто сгенерировать примеры значений полей, проверить выполнение условий, и отбросить неподходящие значения свойств.
Если условие "жёсткое", функциональное (`y == x + 1`), то можно построить функцию, вычисляющую значение второго поля на основе сгенерированного первого.
Если условие "полужесткое" (`y> x + 1 && y < x + 2`), то можно построить зависимый генератор, диапазон значений которого зависит от сгенерированного значения первого поля.

Для того, чтобы собрать все условия, порождающие результат по одной из ветвей, можно воспользоваться "Символическим исполнением" ([Symbolic Execution](https://en.wikipedia.org/wiki/Symbolic_execution). Входные данные принимаются равными некоторым символьным значениям (`field1 = field1_initial_value`). Затем над символьными значениями производятся все манипуляции, описанные в тестируемом коде. Все манипуляции выполняются в символьном же виде. При вычислении условия принимаются возможными оба варианта `true` и `false`, при этом на символьные значения накладываются противоположные ограничения. Например,
```scala
val a = field1 + 10
if(a > 0) A else B
// A -> field_initial_value + 10 > 0
// B -> field_initial_value + 10 <= 0
```
Накопленные в символьном виде ограничения можно использовать либо для формирования генератора, порождающего значения, удовлетворяющие этим ограничениям, либо для проверки случайных значений, формируемых менее точным генератором. В любом случае появляется возможность генерировать случайные данные, приводящие к исполнению заранее известного пути (и, возможно, к известному результату).

### Смысл тестирования белого ящика

При определённом усердии можно добиться того, что тесты, написанные вручную или сгенерированные автоматически, будут покрывать все ветви тестируемого кода, то есть обеспечат 100% покрытие. Тем самым мы сможем с уверенностью сказать, что белый ящик делает то, что он делает. Хм. Секундочку. А в чём, собственно, смысл такого тестирования? Ведь для любого содержимого белого ящика будут построены тесты, которые только лишь подтверждают, что белый ящик работает каким-то определённым образом.

В некоторых случаях такой набор тестов может иметь смысл:
1. Если белый ящик является прототипом и существует ненулевая вероятность ошибки для действительной реализации в другой среде.
2. Если существует несколько реализаций одинаковой логики.
3. Код белого ящика эволюционирует или подвергается рефакторингу, при этом мы хотели бы обнаруживать отличия от эталонного поведения.

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

### Тестирование циклов

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

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

Воспользуемся такой конструкцией как Y-комбинатор (["комбинатор неподвижной точки"](https://ru.wikipedia.org/wiki/Комбинатор_неподвижной_точки)). Комбинатор позволяет реализовать рекурсию в языках, которые рекурсию в чистом виде не поддерживают. (Сам комбинатор является рекурсивным, поэтому должен быть реализован непосредственно в языке.) Работает он следующим образом. Из рекурсивной функции удаляются все рекурсивные вызовы и заменяются на вызовы функции, которая передаётся в качестве дополнительного аргумента. Такая переработанная функция уже не будет являться рекурсивной. Y-комбинатор превращает такую "заготовку рекурсивной функции" в полноценную рекурсивную функцию.

Мы можем воспользоваться аналогичным подходом и переписать тестируемый код, заменив рекурсивные вызовы на вызовы вспомогательной функции. При тестировании мы передадим собственную реализацию вспомогательной функции, которая не будет формировать рекурсию. Такая вспомогательная функция может формировать возвращаемый результат, соответствующий типу возвращаемых значений белого ящика. Например, если возвращаются строки, то вспомогательная функция также будет формировать строку, которую мы сможем проверить в рамках `TestCase`'а. В случае, если тип возвращаемых значений нам окажется неудобен, мы можем выбрасывать исключение (`throw` имеет тип `Nothing` или `bottom`, являющийся подтипом всех остальных).


{'fraction_non_alphanumeric': 0.07208681944949678, 'fraction_numerical': 0.00991269552564569, 'mean_word_length': 5.629622186495177, 'pattern_counts': {'":': 2, '<': 18, '<?xml version=': 0, '>': 51, 'https://': 10, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3724433', 'n_tokens_mistral': 12635, 'n_tokens_neox': 15062, 'n_words': 3777}