Skip to content

Commit

Permalink
Start of DAL implementation, independent of the database
Browse files Browse the repository at this point in the history
Add dal2.HttpStubDAO and impl for MongoDB that hides logic of building
queries to Database.

Added a DSL that lets describe an action sequence and then  generates
scalatest, executing it and markdown files containing curl commands
for these actions.
  • Loading branch information
Aleksei Shashev authored and ashashev committed Aug 8, 2023
1 parent 2a415e3 commit 8087f54
Show file tree
Hide file tree
Showing 40 changed files with 3,357 additions and 114 deletions.
33 changes: 31 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,35 @@ jobs:
name: front-static
path: frontend/dist/out

heavy-tests:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3

- uses: actions/setup-java@v3
with:
distribution: temurin
java-version: 17
check-latest: true

- name: Install Protoc
uses: arduino/setup-protoc@v1
with:
version: '3.20.0'

- name: Get the Ref
id: get-ref
uses: ankitvgupta/[email protected]
with:
ref: ${{ github.ref }}
head_ref: ${{ github.head_ref }}

- name: Test
run: |
cd backend
sbt "project integration;clean;fixCheck;test;"
back-build:
needs: [front-build]

Expand Down Expand Up @@ -142,7 +171,7 @@ jobs:
uses: arduino/setup-protoc@v1
with:
version: '3.20.0'

- name: Get the Ref
id: get-ref
uses: ankitvgupta/[email protected]
Expand All @@ -169,4 +198,4 @@ jobs:
context: ./backend/mockingbird-native/target/docker/stage
push: true
tags: ghcr.io/tinkoff/mockingbird:${{ steps.get-ref.outputs.tag }}-native
if: ${{ github.ref_type == 'tag' || startsWith(github.event.head_commit.message, '[docker]') }}
if: ${{ github.ref_type == 'tag' || startsWith(github.event.head_commit.message, '[docker]') }}
2 changes: 1 addition & 1 deletion backend/.sbtopts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
-J-Xmx1536m
-J-XX:+AlwaysPreTouch
-J-XX:ReservedCodeCacheSize=128M
-J-XX:MaxMetaspaceSize=512M
-J-XX:MaxMetaspaceSize=1024M
-J-Xss2m
-J-XX:+TieredCompilation
-J-XX:+UseParallelGC
Expand Down
80 changes: 79 additions & 1 deletion backend/build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ ThisBuild / evictionErrorLevel := Level.Debug
val utils = (project in file("utils"))
.settings(Settings.common)
.settings(
libraryDependencies ++= Dependencies.zio ++ Dependencies.scalatest ++ Dependencies.metrics
libraryDependencies ++= Dependencies.zio ++ Dependencies.scalatest ++ Dependencies.metrics ++ Dependencies.tofu
)

val circeUtils = (project in file("circe-utils"))
Expand All @@ -20,6 +20,48 @@ val circeUtils = (project in file("circe-utils"))
libraryDependencies ++= Dependencies.json ++ Dependencies.zio ++ Dependencies.scalatest
)

val examples = (project in file("examples"))
.enablePlugins(
JavaAppPackaging
)
.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",
"dev.zio" %% "zio-cli" % "0.5.0",
),
)
.settings(
Compile / doc / sources := (file("examples/src") ** "*.scala").get,
Compile / doc / scalacOptions ++= Seq("-groups", "-skip-packages", "sttp")
)
.settings(
addCommandAlias(
"fixCheck",
"scalafixAll --check; scalafmtCheck"
),
addCommandAlias(
"lintAll",
"scalafixAll; scalafmtAll"
),
addCommandAlias(
"simulacrum",
"scalafixEnable;scalafix AddSerializable;scalafix AddImplicitNotFound;scalafix TypeClassSupport;"
)
)

val dataAccess = (project in file("dataAccess"))
.settings(Settings.common)
.settings(
Expand Down Expand Up @@ -165,6 +207,41 @@ lazy val `mockingbird-native` = (project in file("mockingbird-native"))
)
)

lazy val integration = (project in file("integration"))
.dependsOn(examples)
.dependsOn(mockingbird)
.dependsOn(`mockingbird-api`)
.enablePlugins(
JavaAppPackaging
)
.settings(Settings.common)
.settings(
publish / skip := true,
Test / fork := true,
libraryDependencies ++= Seq(
Dependencies.scalatest,
Dependencies.scalacheck,
).flatten,
libraryDependencies ++= Seq(
"com.dimafeng" %% "testcontainers-scala" % "0.40.17" % Test,
),
Test / javaOptions += "-Dconfig.resource=test.conf",
)
.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 +250,7 @@ val root = (project in file("."))
mockingbird,
`mockingbird-api`,
`mockingbird-native`,
examples
)
.settings(
run / aggregate := false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ class UntypedCriteriaSpec extends AnyFlatSpec with Matchers {
it should "correctly negate and" in {
(
!(criteria.name === "peka" && criteria.id === 42) &&
!(criteria.list in ("a", "b", "c"))
!(criteria.list.in("a", "b", "c"))
).toJson shouldBe BsonDocument(
"$and" -> BsonArray(
BsonDocument(
Expand Down
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.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 8087f54

Please sign in to comment.