Skip to content

Commit

Permalink
Add examples
Browse files Browse the repository at this point in the history
  • Loading branch information
ashashev authored and danslapman committed Oct 21, 2023
1 parent 33efe27 commit a714120
Show file tree
Hide file tree
Showing 28 changed files with 3,419 additions and 3 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ node_modules
dist
.fleet
secrets.conf
/backend/compose-test.yml
57 changes: 57 additions & 0 deletions backend/build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,62 @@ lazy val `mockingbird-native` = (project in file("mockingbird-native"))
)
)

val edsl = (project in file("edsl"))
.dependsOn(utils, circeUtils)
.settings(Settings.common)
.settings(
libraryDependencies ++= Seq(
Dependencies.cats,
Dependencies.tofu,
Dependencies.mouse,
Dependencies.enumeratum,
Dependencies.scalatestMain,
Dependencies.scalamock,
Dependencies.refined,
).flatten,
libraryDependencies ++= Seq(
"com.softwaremill.sttp.client3" %% "armeria-backend-zio" % Versions.sttp,
"com.softwaremill.sttp.client3" %% "circe" % Versions.sttp,
"pl.muninn" %% "scala-md-tag" % "0.2.3",
),
)
.settings(
Compile / doc / sources := (file("edsl/src/main") ** "*.scala").get,
Compile / doc / scalacOptions ++= Seq("-groups", "-skip-packages", "sttp")
)

val examples = (project in file("examples"))
.enablePlugins(
JavaAppPackaging
)
.dependsOn(edsl)
.settings(Settings.common)
.settings(
libraryDependencies ++= Seq(
Dependencies.scalatest,
Dependencies.scalamock,
Dependencies.testContainers,
).flatten,
libraryDependencies ++= Seq(
"dev.zio" %% "zio-cli" % "0.5.0",
),
Test / parallelExecution := false,
)
.settings(
addCommandAlias(
"fixCheck",
"scalafixAll --check; scalafmtCheck"
),
addCommandAlias(
"lintAll",
"scalafixAll; scalafmtAll"
),
addCommandAlias(
"simulacrum",
"scalafixEnable;scalafix AddSerializable;scalafix AddImplicitNotFound;scalafix TypeClassSupport;"
)
)

val root = (project in file("."))
.aggregate(
utils,
Expand All @@ -173,6 +229,7 @@ val root = (project in file("."))
mockingbird,
`mockingbird-api`,
`mockingbird-native`,
`edsl`
)
.settings(
run / aggregate := false,
Expand Down
30 changes: 30 additions & 0 deletions backend/compose-test.yml.jvm.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
services:
mongo:
image: mongo
environment:
- MONGO_INITDB_DATABASE=mockingbird
healthcheck:
test: echo 'db.runCommand("ping").ok' | mongosh mongo:27017/test --quiet
interval: 10s
timeout: 10s
retries: 5
start_period: 10s
networks:
- app-tier

mockingbird:
image: "${MOCKINGBIRD_IMAGE}"
depends_on:
mongo:
condition: service_healthy
volumes:
# Read the docs about secrets
- ./secrets-for-test.conf:/opt/mockingbird/conf/secrets.conf
environment:
- JAVA_OPTS=-server -Xms256m -Xmx256m -XX:MaxDirectMemorySize=128m -Dconfig.resource=qa.conf -Dlog.level=DEBUG -Dlog4j.formatMsgNoLookups=true
networks:
- app-tier

networks:
app-tier:
driver: bridge
28 changes: 28 additions & 0 deletions backend/compose-test.yml.native.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
services:
mongo:
image: mongo
environment:
- MONGO_INITDB_DATABASE=mockingbird
healthcheck:
test: echo 'db.runCommand("ping").ok' | mongosh mongo:27017/test --quiet
interval: 10s
timeout: 10s
retries: 5
start_period: 10s
networks:
- app-tier

mockingbird:
image: "${MOCKINGBIRD_IMAGE}"
depends_on:
mongo:
condition: service_healthy
volumes:
# Read the docs about secrets
- ./secrets-for-test.conf:/opt/mockingbird-native/conf/secrets.conf
command: -server -Xms256m -Xmx256m -XX:MaxDirectMemorySize=128m -Dconfig.file=/opt/mockingbird-native/qa.conf -Dlog.level=DEBUG -Dlog4j.formatMsgNoLookups=true
networks:
- app-tier
networks:
app-tier:
driver: bridge
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
package ru.tinkoff.tcb.mockingbird.edsl

import cats.free.Free.liftF
import org.scalactic.source

import ru.tinkoff.tcb.mockingbird.edsl.model.*

/**
* ==Описание набора примеров==
*
* `ExampleSet` предоставляет DSL для описания примеров взаимодействия с Mockingbird со стороны внешнего
* приложения/пользователя через его API. Описанные примеры потом можно в Markdown описание последовательности действий
* с примерами HTTP запросов и ответов на них или сгенерировать тесты для scalatest. За это отвечают интерпретаторы DSL
* [[ru.tinkoff.tcb.mockingbird.edsl.interpreter.MarkdownGenerator MarkdownGenerator]] и
* [[ru.tinkoff.tcb.mockingbird.edsl.interpreter.AsyncScalaTestSuite AsyncScalaTestSuite]] соответственно.
*
* Описание набора примеров может выглядеть так:
*
* {{{
* package ru.tinkoff.tcb.mockingbird.examples
*
* import ru.tinkoff.tcb.mockingbird.edsl.ExampleSet
* import ru.tinkoff.tcb.mockingbird.edsl.model.*
* import ru.tinkoff.tcb.mockingbird.edsl.model.Check.*
* import ru.tinkoff.tcb.mockingbird.edsl.model.HttpMethod.*
* import ru.tinkoff.tcb.mockingbird.edsl.model.ValueMatcher.syntax.*
*
* class CatsFacts[HttpResponseR] extends ExampleSet[HttpResponseR] {
*
* override val name = "Примеры использования ExampleSet"
*
* example("Получение случайного факта о котиках")(
* for {
* _ <- describe("Отправить GET запрос")
* resp <- sendHttp(
* method = Get,
* path = "/fact",
* headers = Seq("X-CSRF-TOKEN" -> "unEENxJqSLS02rji2GjcKzNLc0C0ySlWih9hSxwn")
* )
* _ <- describe("Ответ содержит случайный факт полученный с сервера")
* _ <- checkHttp(
* resp,
* HttpResponseExpected(
* code = Some(CheckInteger(200)),
* body = Some(
* CheckJsonObject(
* "fact" -> CheckJsonString("There are approximately 100 breeds of cat.".sample),
* "length" -> CheckJsonNumber(42.sample)
* )
* ),
* headers = Seq("Content-Type" -> CheckString("application/json"))
* )
* )
* } yield ()
* )
* }
* }}}
*
* Дженерик параметр `HttpResponseR` нужен так результат выполнения HTTP запроса зависит от интерпретатора DSL.
*
* Переменная `name` - общий заголовок для примеров внутри набора, при генерации Markdown файла будет добавлен в самое
* начало как заголовок первого уровня.
*
* Метод `example` позволяет добавить пример к набору. Вначале указывается название примера, как первый набор
* аргументов. При генерации тестов это будет именем теста, а при генерации Markdown будет добавлено как заголовок
* второго уровня, затем описывается сам пример. Последовательность действий описывается при помощи монады
* [[ru.tinkoff.tcb.mockingbird.edsl.model.Example Example]].
*
* `ExampleSet` предоставляет следующие действия:
* - [[describe]] - добавить текстовое описание.
* - [[sendHttp]] - исполнить HTTP запрос с указанными параметрами, возвращает результат запроса.
* - [[checkHttp]] - проверить, что результат запроса отвечает указанным ожиданиям, возвращает извлеченные из ответа
* данные на основании проверок. ''Если предполагается использовать какие-то части ответа по ходу описания примера,
* то необходимо для них задать ожидания, иначе они будут отсутствовать в возвращаемом объекте.''
*
* Для описания ожиданий используются проверки [[model.Check$]]. Некоторые проверки принимают как параметр
* [[model.ValueMatcher ValueMatcher]]. Данный трейт тип представлен двумя реализациями
* [[model.ValueMatcher.AnyValue AnyValue]] и [[model.ValueMatcher.FixedValue FixedValue]]. Первая описывает
* произвольное значение определенного типа, т.е. проверки значения не производится. Вторая задает конкретное ожидаемое
* значение.
*
* Для упрощения создания значений типа [[model.ValueMatcher ValueMatcher]] добавлены имплиситы в объекте
* [[model.ValueMatcher.syntax ValueMatcher.syntax]]. Они добавляют неявную конвертацию значений в тип
* [[model.ValueMatcher.FixedValue FixedValue]], а так же методы `sample` и `fixed` для создания
* [[model.ValueMatcher.AnyValue AnyValue]] и [[model.ValueMatcher.FixedValue FixedValue]] соответственно. Благодаря
* этому можно писать:
* {{{
* CheckString("some sample".sample) // вместо CheckString(AnyValue("some sample"))
* CheckString("some fixed string") // вместо CheckString(FixedValue("some fixed string"))
* }}}
*
* ==Генерации markdown документа из набора примеров==
*
* {{{
* package ru.tinkoff.tcb.mockingbird.examples
*
* import sttp.client3.*
*
* import ru.tinkoff.tcb.mockingbird.edsl.interpreter.MarkdownGenerator
*
* object CatsFactsMd {
* def main(args: Array[String]): Unit = {
* val mdg = MarkdownGenerator(baseUri = uri"https://catfact.ninja")
* val set = new CatsFacts[MarkdownGenerator.HttpResponseR]()
* println(mdg.generate(set))
* }
* }
* }}}
*
* Здесь создается интерпретатор [[ru.tinkoff.tcb.mockingbird.edsl.interpreter.MarkdownGenerator MarkdownGenerator]] для
* генерации markdown документа из инстанса `ExampleSet`. Как параметр, конструктору передается хост со схемой который
* будет подставлен в качестве примера в документ.
*
* Как упоминалось ранее, тип ответа от HTTP сервера зависит от интерпретатора DSL, поэтому при создании `CatsFacts`
* параметром передается тип `MarkdownGenerator.HttpResponseR`.
*
* ==Генерация тестов из набора примеров==
* {{{
* package ru.tinkoff.tcb.mockingbird.examples
*
* import sttp.client3.*
*
* import ru.tinkoff.tcb.mockingbird.edsl.interpreter.AsyncScalaTestSuite
*
* class CatsFactsSuite extends AsyncScalaTestSuite {
* override val baseUri = uri"https://catfact.ninja"
* val set = new CatsFacts[HttpResponseR]()
* generateTests(set)
* }
* }}}
*
* Для генерации тестов нужно создать класс и унаследовать его от
* [[ru.tinkoff.tcb.mockingbird.edsl.interpreter.AsyncScalaTestSuite AsyncScalaTestSuite]]. После чего в переопределить
* значение `baseUri` и в конструкторе вызвать метод `generateTests` передав в него набор примеров. В качестве дженерик
* параметра для типа HTTP ответа, в создаваемый инстанс набора примеров надо передать тип
* [[ru.tinkoff.tcb.mockingbird.edsl.interpreter.AsyncScalaTestSuite.HttpResponseR AsyncScalaTestSuite.HttpResponseR]]
*
* Пример запуска тестов:
* {{{
* [info] CatsFactsSuite:
* [info] - Получение случайного факта о котиках
* [info] + Отправить GET запрос
* [info] + Ответ содержит случайный факт полученный с сервера
* [info] Run completed in 563 milliseconds.
* [info] Total number of tests run: 1
* [info] Suites: completed 1, aborted 0
* [info] Tests: succeeded 1, failed 0, canceled 0, ignored 0, pending 0
* [info] All tests passed.
* }}}
*/
trait ExampleSet[HttpResponseR] {
private var examples_ : Vector[ExampleDescription] = Vector.empty

final private[edsl] def examples: Vector[ExampleDescription] = examples_

/**
* Заглавие набора примеров.
*/
def name: String

final protected def example(name: String)(body: Example[Any])(implicit pos: source.Position): Unit =
examples_ = examples_ :+ ExampleDescription(name, body, pos)

/**
* Выводит сообщение при помощи `info` при генерации тестов или добавляет текстовый блок при генерации Markdown.
* @param text
* текст сообщения
*/
final def describe(text: String)(implicit pos: source.Position): Example[Unit] =
liftF[Step, Unit](Describe(text, pos))

/**
* В тестах, выполняет HTTP запрос с указанными параметрами или добавляет в Markdown пример запроса, который можно
* исполнить командой `curl`.
*
* @param method
* используемый HTTP метод.
* @param path
* путь до ресурса без схемы и хоста.
* @param body
* тело запроса как текст.
* @param headers
* заголовки, который будут переданы вместе с запросом.
* @param query
* URL параметры запроса
* @return
* возвращает объект представляющий собой результат исполнения запроса, конкретный тип зависит от интерпретатора
* DSL. Использовать возвращаемое значение можно только передав в метод [[checkHttp]].
*/
final def sendHttp(
method: HttpMethod,
path: String,
body: Option[String] = None,
headers: Seq[(String, String)] = Seq.empty,
query: Seq[(String, String)] = Seq.empty,
)(implicit
pos: source.Position
): Example[HttpResponseR] =
liftF[Step, HttpResponseR](SendHttp[HttpResponseR](HttpRequest(method, path, body, headers, query), pos))

/**
* В тестах, проверяет, что полученный HTTP ответ соответствует ожиданиям. При генерации Markdown вставляет ожидаемый
* ответ опираясь на указанные ожидания. Если никакие ожидания не указана, то ничего добавлено не будет.
*
* @param response
* результат исполнения [[sendHttp]], тип зависит от интерпретатора DSL.
* @param expects
* ожидания предъявляемые к результату HTTP запроса. Ожидания касаются кода ответа, тела запроса и заголовков
* полеченных от сервера.
* @return
* возвращает разобранный ответ от сервера. При генерации Markdown, так как реального ответа от сервера нет, то
* формирует ответ на основании переданных ожиданий от ответа. В Markdown добавляется информация только от том, для
* чего была указана проверка.
*/
final def checkHttp(response: HttpResponseR, expects: HttpResponseExpected)(implicit
pos: source.Position
): Example[HttpResponse] =
liftF[Step, HttpResponse](CheckHttp(response, expects, pos))

}
Loading

0 comments on commit a714120

Please sign in to comment.