|
| 1 | +package ru.tinkoff.tcb.mockingbird.edsl |
| 2 | + |
| 3 | +import cats.free.Free.liftF |
| 4 | +import org.scalactic.source |
| 5 | + |
| 6 | +import ru.tinkoff.tcb.mockingbird.edsl.model.* |
| 7 | + |
| 8 | +/** |
| 9 | + * ==Описание набора примеров== |
| 10 | + * |
| 11 | + * `ExampleSet` предоставляет DSL для описания примеров взаимодействия с Mockingbird со стороны внешнего |
| 12 | + * приложения/пользователя через его API. Описанные примеры потом можно в Markdown описание последовательности действий |
| 13 | + * с примерами HTTP запросов и ответов на них или сгенерировать тесты для scalatest. За это отвечают интерпретаторы DSL |
| 14 | + * [[ru.tinkoff.tcb.mockingbird.edsl.interpreter.MarkdownGenerator MarkdownGenerator]] и |
| 15 | + * [[ru.tinkoff.tcb.mockingbird.edsl.interpreter.AsyncScalaTestSuite AsyncScalaTestSuite]] соответственно. |
| 16 | + * |
| 17 | + * Описание набора примеров может выглядеть так: |
| 18 | + * |
| 19 | + * {{{ |
| 20 | + * package ru.tinkoff.tcb.mockingbird.examples |
| 21 | + * |
| 22 | + * import ru.tinkoff.tcb.mockingbird.edsl.ExampleSet |
| 23 | + * import ru.tinkoff.tcb.mockingbird.edsl.model.* |
| 24 | + * import ru.tinkoff.tcb.mockingbird.edsl.model.Check.* |
| 25 | + * import ru.tinkoff.tcb.mockingbird.edsl.model.HttpMethod.* |
| 26 | + * import ru.tinkoff.tcb.mockingbird.edsl.model.ValueMatcher.syntax.* |
| 27 | + * |
| 28 | + * class CatsFacts[HttpResponseR] extends ExampleSet[HttpResponseR] { |
| 29 | + * |
| 30 | + * override val name = "Примеры использования ExampleSet" |
| 31 | + * |
| 32 | + * example("Получение случайного факта о котиках")( |
| 33 | + * for { |
| 34 | + * _ <- describe("Отправить GET запрос") |
| 35 | + * resp <- sendHttp( |
| 36 | + * method = Get, |
| 37 | + * path = "/fact", |
| 38 | + * headers = Seq("X-CSRF-TOKEN" -> "unEENxJqSLS02rji2GjcKzNLc0C0ySlWih9hSxwn") |
| 39 | + * ) |
| 40 | + * _ <- describe("Ответ содержит случайный факт полученный с сервера") |
| 41 | + * _ <- checkHttp( |
| 42 | + * resp, |
| 43 | + * HttpResponseExpected( |
| 44 | + * code = Some(CheckInteger(200)), |
| 45 | + * body = Some( |
| 46 | + * CheckJsonObject( |
| 47 | + * "fact" -> CheckJsonString("There are approximately 100 breeds of cat.".sample), |
| 48 | + * "length" -> CheckJsonNumber(42.sample) |
| 49 | + * ) |
| 50 | + * ), |
| 51 | + * headers = Seq("Content-Type" -> CheckString("application/json")) |
| 52 | + * ) |
| 53 | + * ) |
| 54 | + * } yield () |
| 55 | + * ) |
| 56 | + * } |
| 57 | + * }}} |
| 58 | + * |
| 59 | + * Дженерик параметр `HttpResponseR` нужен так результат выполнения HTTP запроса зависит от интерпретатора DSL. |
| 60 | + * |
| 61 | + * Переменная `name` - общий заголовок для примеров внутри набора, при генерации Markdown файла будет добавлен в самое |
| 62 | + * начало как заголовок первого уровня. |
| 63 | + * |
| 64 | + * Метод `example` позволяет добавить пример к набору. Вначале указывается название примера, как первый набор |
| 65 | + * аргументов. При генерации тестов это будет именем теста, а при генерации Markdown будет добавлено как заголовок |
| 66 | + * второго уровня, затем описывается сам пример. Последовательность действий описывается при помощи монады |
| 67 | + * [[ru.tinkoff.tcb.mockingbird.edsl.Example Example]]. |
| 68 | + * |
| 69 | + * `ExampleSet` предоставляет следующие действия: |
| 70 | + * - [[describe]] - добавить текстовое описание. |
| 71 | + * - [[sendHttp]] - исполнить HTTP запрос с указанными параметрами, возвращает результат запроса. |
| 72 | + * - [[checkHttp]] - проверить, что результат запроса отвечает указанным ожиданиям, возвращает извлеченные из ответа |
| 73 | + * данные на основании проверок. ''Если предполагается использовать какие-то части ответа по ходу описания примера, |
| 74 | + * то необходимо для них задать ожидания, иначе они будут отсутствовать в возвращаемом объекте.'' |
| 75 | + * |
| 76 | + * Для описания ожиданий используются проверки [[model.Check$]]. Некоторые проверки принимают как параметр |
| 77 | + * [[model.ValueMatcher ValueMatcher]]. Данный трейт тип представлен двумя реализациями |
| 78 | + * [[model.ValueMatcher.AnyValue AnyValue]] и [[model.ValueMatcher.FixedValue FixedValue]]. Первая описывает |
| 79 | + * произвольное значение определенного типа, т.е. проверки значения не производится. Вторая задает конкретное ожидаемое |
| 80 | + * значение. |
| 81 | + * |
| 82 | + * Для упрощения создания значений типа [[model.ValueMatcher ValueMatcher]] добавлены имплиситы в объекте |
| 83 | + * [[model.ValueMatcher.syntax ValueMatcher.syntax]]. Они добавляют неявную конвертацию значений в тип |
| 84 | + * [[model.ValueMatcher.FixedValue FixedValue]], а так же методы `sample` и `fixed` для создания |
| 85 | + * [[model.ValueMatcher.AnyValue AnyValue]] и [[model.ValueMatcher.FixedValue FixedValue]] соответственно. Благодаря |
| 86 | + * этому можно писать: |
| 87 | + * {{{ |
| 88 | + * CheckString("some sample".sample) // вместо CheckString(AnyValue("some sample")) |
| 89 | + * CheckString("some fixed string") // вместо CheckString(FixedValue("some fixed string")) |
| 90 | + * }}} |
| 91 | + * |
| 92 | + * ==Генерации markdown документа из набора примеров== |
| 93 | + * |
| 94 | + * {{{ |
| 95 | + * package ru.tinkoff.tcb.mockingbird.examples |
| 96 | + * |
| 97 | + * import sttp.client3.* |
| 98 | + * |
| 99 | + * import ru.tinkoff.tcb.mockingbird.edsl.interpreter.MarkdownGenerator |
| 100 | + * |
| 101 | + * object CatsFactsMd { |
| 102 | + * def main(args: Array[String]): Unit = { |
| 103 | + * val mdg = MarkdownGenerator(baseUri = uri"https://catfact.ninja") |
| 104 | + * val set = new CatsFacts[MarkdownGenerator.HttpResponseR]() |
| 105 | + * println(mdg.generate(set)) |
| 106 | + * } |
| 107 | + * } |
| 108 | + * }}} |
| 109 | + * |
| 110 | + * Здесь создается интерпретатор [[ru.tinkoff.tcb.mockingbird.edsl.interpreter.MarkdownGenerator MarkdownGenerator]] для |
| 111 | + * генерации markdown документа из инстанса `ExampleSet`. Как параметр, конструктору передается хост со схемой который |
| 112 | + * будет подставлен в качестве примера в документ. |
| 113 | + * |
| 114 | + * Как упоминалось ранее, тип ответа от HTTP сервера зависит от интерпретатора DSL, поэтому при создании `CatsFacts` |
| 115 | + * параметром передается тип `MarkdownGenerator.HttpResponseR`. |
| 116 | + * |
| 117 | + * ==Генерация тестов из набора примеров== |
| 118 | + * {{{ |
| 119 | + * package ru.tinkoff.tcb.mockingbird.examples |
| 120 | + * |
| 121 | + * import sttp.client3.* |
| 122 | + * |
| 123 | + * import ru.tinkoff.tcb.mockingbird.edsl.interpreter.AsyncScalaTestSuite |
| 124 | + * |
| 125 | + * class CatsFactsSuite extends AsyncScalaTestSuite { |
| 126 | + * override val baseUri = uri"https://catfact.ninja" |
| 127 | + * val set = new CatsFacts[HttpResponseR]() |
| 128 | + * generateTests(set) |
| 129 | + * } |
| 130 | + * }}} |
| 131 | + * |
| 132 | + * Для генерации тестов нужно создать класс и унаследовать его от |
| 133 | + * [[ru.tinkoff.tcb.mockingbird.edsl.interpreter.AsyncScalaTestSuite AsyncScalaTestSuite]]. После чего в переопределить |
| 134 | + * значение `baseUri` и в конструкторе вызвать метод `generateTests` передав в него набор примеров. В качестве дженерик |
| 135 | + * параметра для типа HTTP ответа, в создаваемый инстанс набора примеров надо передать тип |
| 136 | + * [[ru.tinkoff.tcb.mockingbird.edsl.interpreter.AsyncScalaTestSuite.HttpResponseR AsyncScalaTestSuite.HttpResponseR]] |
| 137 | + * |
| 138 | + * Пример запуска тестов: |
| 139 | + * {{{ |
| 140 | + * [info] CatsFactsSuite: |
| 141 | + * [info] - Получение случайного факта о котиках |
| 142 | + * [info] + Отправить GET запрос |
| 143 | + * [info] + Ответ содержит случайный факт полученный с сервера |
| 144 | + * [info] Run completed in 563 milliseconds. |
| 145 | + * [info] Total number of tests run: 1 |
| 146 | + * [info] Suites: completed 1, aborted 0 |
| 147 | + * [info] Tests: succeeded 1, failed 0, canceled 0, ignored 0, pending 0 |
| 148 | + * [info] All tests passed. |
| 149 | + * }}} |
| 150 | + */ |
| 151 | +trait ExampleSet[HttpResponseR] { |
| 152 | + private var examples_ : Vector[ExampleDescription] = Vector.empty |
| 153 | + |
| 154 | + final private[edsl] def examples: Vector[ExampleDescription] = examples_ |
| 155 | + |
| 156 | + /** |
| 157 | + * Заглавие набора примеров. |
| 158 | + */ |
| 159 | + def name: String |
| 160 | + |
| 161 | + final protected def example(name: String)(body: Example[Any])(implicit pos: source.Position): Unit = |
| 162 | + examples_ = examples_ :+ ExampleDescription(name, body, pos) |
| 163 | + |
| 164 | + /** |
| 165 | + * Выводит сообщение при помощи `info` при генерации тестов или добавляет текстовый блок при генерации Markdown. |
| 166 | + * @param text |
| 167 | + * текст сообщения |
| 168 | + */ |
| 169 | + final def describe(text: String)(implicit pos: source.Position): Example[Unit] = |
| 170 | + liftF[Step, Unit](Describe(text, pos)) |
| 171 | + |
| 172 | + /** |
| 173 | + * В тестах, выполняет HTTP запрос с указанными параметрами или добавляет в Markdown пример запроса, который можно |
| 174 | + * исполнить командой `curl`. |
| 175 | + * |
| 176 | + * @param method |
| 177 | + * используемый HTTP метод. |
| 178 | + * @param path |
| 179 | + * путь до ресурса без схемы и хоста. |
| 180 | + * @param body |
| 181 | + * тело запроса как текст. |
| 182 | + * @param headers |
| 183 | + * заголовки, который будут переданы вместе с запросом. |
| 184 | + * @param query |
| 185 | + * URL параметры запроса |
| 186 | + * @return |
| 187 | + * возвращает объект представляющий собой результат исполнения запроса, конкретный тип зависит от интерпретатора |
| 188 | + * DSL. Использовать возвращаемое значение можно только передав в метод [[checkHttp]]. |
| 189 | + */ |
| 190 | + final def sendHttp( |
| 191 | + method: HttpMethod, |
| 192 | + path: String, |
| 193 | + body: Option[String] = None, |
| 194 | + headers: Seq[(String, String)] = Seq.empty, |
| 195 | + query: Seq[(String, String)] = Seq.empty, |
| 196 | + )(implicit |
| 197 | + pos: source.Position |
| 198 | + ): Example[HttpResponseR] = |
| 199 | + liftF[Step, HttpResponseR](SendHttp[HttpResponseR](HttpRequest(method, path, body, headers, query), pos)) |
| 200 | + |
| 201 | + /** |
| 202 | + * В тестах, проверяет, что полученный HTTP ответ соответствует ожиданиям. При генерации Markdown вставляет ожидаемый |
| 203 | + * ответ опираясь на указанные ожидания. Если никакие ожидания не указана, то ничего добавлено не будет. |
| 204 | + * |
| 205 | + * @param response |
| 206 | + * результат исполнения [[sendHttp]], тип зависит от интерпретатора DSL. |
| 207 | + * @param expects |
| 208 | + * ожидания предъявляемые к результату HTTP запроса. Ожидания касаются кода ответа, тела запроса и заголовков |
| 209 | + * полеченных от сервера. |
| 210 | + * @return |
| 211 | + * возвращает разобранный ответ от сервера. При генерации Markdown, так как реального ответа от сервера нет, то |
| 212 | + * формирует ответ на основании переданных ожиданий от ответа. В Markdown добавляется информация только от том, для |
| 213 | + * чего была указана проверка. |
| 214 | + */ |
| 215 | + final def checkHttp(response: HttpResponseR, expects: HttpResponseExpected)(implicit |
| 216 | + pos: source.Position |
| 217 | + ): Example[HttpResponse] = |
| 218 | + liftF[Step, HttpResponse](CheckHttp(response, expects, pos)) |
| 219 | + |
| 220 | +} |
0 commit comments