Skip to content

Commit 8087f54

Browse files
Aleksei Shashevashashev
authored andcommitted
Start of DAL implementation, independent of the database
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.
1 parent 2a415e3 commit 8087f54

File tree

40 files changed

+3357
-114
lines changed

40 files changed

+3357
-114
lines changed

.github/workflows/ci.yml

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,35 @@ jobs:
4545
name: front-static
4646
path: frontend/dist/out
4747

48+
heavy-tests:
49+
runs-on: ubuntu-latest
50+
51+
steps:
52+
- uses: actions/checkout@v3
53+
54+
- uses: actions/setup-java@v3
55+
with:
56+
distribution: temurin
57+
java-version: 17
58+
check-latest: true
59+
60+
- name: Install Protoc
61+
uses: arduino/setup-protoc@v1
62+
with:
63+
version: '3.20.0'
64+
65+
- name: Get the Ref
66+
id: get-ref
67+
uses: ankitvgupta/[email protected]
68+
with:
69+
ref: ${{ github.ref }}
70+
head_ref: ${{ github.head_ref }}
71+
72+
- name: Test
73+
run: |
74+
cd backend
75+
sbt "project integration;clean;fixCheck;test;"
76+
4877
back-build:
4978
needs: [front-build]
5079

@@ -142,7 +171,7 @@ jobs:
142171
uses: arduino/setup-protoc@v1
143172
with:
144173
version: '3.20.0'
145-
174+
146175
- name: Get the Ref
147176
id: get-ref
148177
uses: ankitvgupta/[email protected]
@@ -169,4 +198,4 @@ jobs:
169198
context: ./backend/mockingbird-native/target/docker/stage
170199
push: true
171200
tags: ghcr.io/tinkoff/mockingbird:${{ steps.get-ref.outputs.tag }}-native
172-
if: ${{ github.ref_type == 'tag' || startsWith(github.event.head_commit.message, '[docker]') }}
201+
if: ${{ github.ref_type == 'tag' || startsWith(github.event.head_commit.message, '[docker]') }}

backend/.sbtopts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
-J-Xmx1536m
33
-J-XX:+AlwaysPreTouch
44
-J-XX:ReservedCodeCacheSize=128M
5-
-J-XX:MaxMetaspaceSize=512M
5+
-J-XX:MaxMetaspaceSize=1024M
66
-J-Xss2m
77
-J-XX:+TieredCompilation
88
-J-XX:+UseParallelGC

backend/build.sbt

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ ThisBuild / evictionErrorLevel := Level.Debug
1111
val utils = (project in file("utils"))
1212
.settings(Settings.common)
1313
.settings(
14-
libraryDependencies ++= Dependencies.zio ++ Dependencies.scalatest ++ Dependencies.metrics
14+
libraryDependencies ++= Dependencies.zio ++ Dependencies.scalatest ++ Dependencies.metrics ++ Dependencies.tofu
1515
)
1616

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

23+
val examples = (project in file("examples"))
24+
.enablePlugins(
25+
JavaAppPackaging
26+
)
27+
.dependsOn(utils, circeUtils)
28+
.settings(Settings.common)
29+
.settings(
30+
libraryDependencies ++= Seq(
31+
Dependencies.cats,
32+
Dependencies.tofu,
33+
Dependencies.mouse,
34+
Dependencies.enumeratum,
35+
Dependencies.scalatestMain,
36+
Dependencies.scalamock,
37+
Dependencies.refined,
38+
).flatten,
39+
libraryDependencies ++= Seq(
40+
"com.softwaremill.sttp.client3" %% "armeria-backend-zio" % Versions.sttp,
41+
"com.softwaremill.sttp.client3" %% "circe" % Versions.sttp,
42+
"pl.muninn" %% "scala-md-tag" % "0.2.3",
43+
"dev.zio" %% "zio-cli" % "0.5.0",
44+
),
45+
)
46+
.settings(
47+
Compile / doc / sources := (file("examples/src") ** "*.scala").get,
48+
Compile / doc / scalacOptions ++= Seq("-groups", "-skip-packages", "sttp")
49+
)
50+
.settings(
51+
addCommandAlias(
52+
"fixCheck",
53+
"scalafixAll --check; scalafmtCheck"
54+
),
55+
addCommandAlias(
56+
"lintAll",
57+
"scalafixAll; scalafmtAll"
58+
),
59+
addCommandAlias(
60+
"simulacrum",
61+
"scalafixEnable;scalafix AddSerializable;scalafix AddImplicitNotFound;scalafix TypeClassSupport;"
62+
)
63+
)
64+
2365
val dataAccess = (project in file("dataAccess"))
2466
.settings(Settings.common)
2567
.settings(
@@ -165,6 +207,41 @@ lazy val `mockingbird-native` = (project in file("mockingbird-native"))
165207
)
166208
)
167209

210+
lazy val integration = (project in file("integration"))
211+
.dependsOn(examples)
212+
.dependsOn(mockingbird)
213+
.dependsOn(`mockingbird-api`)
214+
.enablePlugins(
215+
JavaAppPackaging
216+
)
217+
.settings(Settings.common)
218+
.settings(
219+
publish / skip := true,
220+
Test / fork := true,
221+
libraryDependencies ++= Seq(
222+
Dependencies.scalatest,
223+
Dependencies.scalacheck,
224+
).flatten,
225+
libraryDependencies ++= Seq(
226+
"com.dimafeng" %% "testcontainers-scala" % "0.40.17" % Test,
227+
),
228+
Test / javaOptions += "-Dconfig.resource=test.conf",
229+
)
230+
.settings(
231+
addCommandAlias(
232+
"fixCheck",
233+
"scalafixAll --check; scalafmtCheck"
234+
),
235+
addCommandAlias(
236+
"lintAll",
237+
"scalafixAll; scalafmtAll"
238+
),
239+
addCommandAlias(
240+
"simulacrum",
241+
"scalafixEnable;scalafix AddSerializable;scalafix AddImplicitNotFound;scalafix TypeClassSupport;"
242+
)
243+
)
244+
168245
val root = (project in file("."))
169246
.aggregate(
170247
utils,
@@ -173,6 +250,7 @@ val root = (project in file("."))
173250
mockingbird,
174251
`mockingbird-api`,
175252
`mockingbird-native`,
253+
examples
176254
)
177255
.settings(
178256
run / aggregate := false,

backend/dataAccess/src/test/scala/ru/tinkoff/tcb/criteria/UntypedCriteriaSpec.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,7 @@ class UntypedCriteriaSpec extends AnyFlatSpec with Matchers {
285285
it should "correctly negate and" in {
286286
(
287287
!(criteria.name === "peka" && criteria.id === 42) &&
288-
!(criteria.list in ("a", "b", "c"))
288+
!(criteria.list.in("a", "b", "c"))
289289
).toJson shouldBe BsonDocument(
290290
"$and" -> BsonArray(
291291
BsonDocument(
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
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

Comments
 (0)