Начало новой истории для изменения списка

Отрывок из книги От объектов к функциям Уберто Барбини

In this excerpt:
* Work-in-Progress
* Domain-Driven Test Process
    * Actor Step
    * HTTP Actions Call
    * Handle Different Pages

Давайте рассмотрим вторую историю: добавление элемента в список. Написать доменно-ориентированный тест (DDT) несложно, но начинать с нуля может быть немного сложно. Хитрость заключается в том, чтобы мыслить в терминах сценария использования: как бы мы описали его, используя шаги мелких актеров? Чтобы скомпилировать DDT, мы можем добавлять шаги в качестве пустых функций в актере, пока мы не будем удовлетворены кодом сценария.

❶ class ModifyAToDoListDDT: ZettaiDDT(allActions()){
    val ann by NamedActor(::ToDoListOwner) 
    @DDT
❷   fun `The list owner can add new items`() = ddtScenario {
        setup {
❸           ann.`starts with a list`("diy", emptyList())
        }.thenPlay(
❹           ann.`can add #item to #listname`("paint the shelf", "diy"), 
            ann.`can add #item to #listname`("fix the gate", "diy"), 
            ann.`can add #item to #listname`("change the lock", "diy"),
❺           ann.`can see #listname with #itemnames`("diy", listOf(
              "fix the gate", "paint the shelf", "change the lock"))
❻       ).wip(LocalDate.of(2023,12,31), "Not implemented yet")
    }
  }

❶ Имя тестового класса отражает имя пользовательской истории.

❷ Каждый тест представляет собой сценарий истории.

❸ Начнем со списка без пунктов.

❹ Затем мы трижды вызываем новый шаг для добавления элемента.

❺ Наконец, мы проверяем, что в списке есть три элемента.

❻ Мы отмечаем тест как незавершенный, пока он не пройдет.

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

Обратите внимание, как мы пометили новый тест методомwipв конце. Это сокращение отнезавершенной работы, поскольку, как мы говорили в первой главе, наш DDT не пройдет проверку, пока мы не завершим реализацию в конце этой главы.

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

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

Работа в процессе

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

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

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

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

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

Процесс доменно-ориентированного тестирования

Процесс работы с ДДТ выглядит как буква V:

  1. Сначала мы начнем с версии Http, чтобы разобраться в «сантехнике» нашей архитектуры.
  2. Когда мы подходим к моменту, когда нам нужна некоторая логика предметной области, мы переключаемся на InMemory/DomainOnlyDDT и позволяем им помочь нам в моделировании предметной области.
  3. Затем мы разрабатываем необходимые компоненты один за другим, используя модульные тесты.
  4. После этого фиксируем DomainOnly ДДТ до тех пор, пока они не пройдут.
  5. Наконец, мы возвращаемся к Http DDT и убеждаемся, что окончательная инфраструктура работает должным образом.

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

😎 Джо спрашивает:Почему мы начинаем с HTTP, а не с хаба?

Как мы обсуждали в первой главе, написание функций, начиная с внешних уровней (UI) и заканчивая внутренними, называется стилем «снаружи внутри», тогда как написание их из внутренней области и переходом к внешним уровням. называется стилем «наизнанку».

Какой стиль нам следует использовать? Однозначного ответа не существует, и он действительно зависит от наших ограничений и критериев приемки. DDT определяются действиями пользователя на внешнем уровне системы; поэтому имеет смысл начинать с уровня HTTP.

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

Актер Степ

Чтобы скомпилировать его, нам нужно добавить новый шаг к актеру:

data class ToDoListOwner(override val name: String):
DdtActor<ZettaiActions>() {
val user = User(name)
fun `can add #item to #listname`(itemName: String, listName: String) = step(itemName, listName) {
val item = ToDoItem(itemName) addListItem(user, ListName(listName), item)
}
//rest of the methods
}

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

Это общая закономерность. Шаги актера содержат только логику для вызова действий, но они не взаимодействуют с приложением напрямую. Здесь мы отправляем команду приложению, поэтому у нас нет результата для проверки. В случае шагов, которые запрашивают статус приложения — как в случае с шагом can see #listname с #itemnames — мы также проверим, соответствует ли результат ожидаемому.

Вызов действий HTTP

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

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

Вместо этого мы начнем с реализации HTTP. Чтобы имитировать добавление элемента в список, нам нужно отправить на сервер веб-форму HTTP с полями item name и item due date:

data class HttpActions(val env: String = "local"): ZettaiActions {
    override fun addListItem(user: User, 
            listName: ListName, item: ToDoItem) {

        val response = submitToZettai( 
            todoListUrl(user, listName),
            listOf( "itemname" to item.description,
                    "itemdue" to item.dueDate?.toString())
        )
        expectThat(response.status).isEqualTo(Status.SEE_OTHER)
    }

private fun submitToZettai(path: String, webForm: Form): Response = 
    client(log(
        Request(
            Method.POST,
            "http://localhost:$zettaiPort/$path")
       .body(webForm.toBody())))

//rest of methods…
}

Поскольку DDT теперь компилируется, мы можем его запустить. Вот как это выглядит в IDE:

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

Обработка разных страниц

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

Но у нас возникла новая проблема: до сих пор мы создали функцию, которая возвращает HTML-страницу со списком дел из запроса, но это лишь один из многих видов запросов, которые нашему веб-сервису придется обрабатывать. Как мы можем вернуть разные страницы или API в соответствии с деталями запроса?

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

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

Точнее, нам нужна функция, которая принимает на вход набор функций и возвращает новую функцию.

Мы надеемся, что вам понравился этот отрывок из книги От объектов к функциям Уберто Барбини. Вы можете приобрести электронную книгу непосредственно на сайте The Pragmatic Bookshelf:



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