diff --git a/kotlin-reporter-junit/.gitignore b/kotlin-reporter-junit/.gitignore
new file mode 100644
index 0000000..2e0882f
--- /dev/null
+++ b/kotlin-reporter-junit/.gitignore
@@ -0,0 +1,2 @@
+**/logs/
+**/target/
\ No newline at end of file
diff --git a/kotlin-reporter-junit/README.md b/kotlin-reporter-junit/README.md
new file mode 100644
index 0000000..4473944
--- /dev/null
+++ b/kotlin-reporter-junit/README.md
@@ -0,0 +1,51 @@
+# Kotlin reporter integration with JUnit5
+
+## Overview
+
+This simple demo shows how Testomat.io Kotlin reporter works in your project.
+
+- Some will fail on purpose and other will be disabled for demo.
+
+## Installation
+
+1. Clone the repository
+
+```sh
+ git clone https://github.com/testomatio/examples.git
+ ```
+2. Change the directory
+
+```sh
+ cd Kotlin-reporter-junit
+```
+3. Install dependencies with test skip
+
+```sh
+ mvn clean install -DskipTests
+```
+
+
+## Configurations
+
+**By default, the library runs with properties default values except `testomatio.api.key` and `testomatio.listening`**
+
+
+
+Add your project API key to the `testomatio.properties` file ad `testomatio.api.key`
+
+## Run
+
+Run tests with
+
+```bash
+ mvn test -Dtestomatio.api.key=tstmt_key #if you did not provide it in the `testomatio.properties` file
+```
+
+where `tstmt_key` is your Testomat.io key from a particular project.
+
+As a result, you will see a run report in your Project tab -> Runs on Testomat.io.
+
+
+

+
+
diff --git a/kotlin-reporter-junit/img/properties.png b/kotlin-reporter-junit/img/properties.png
new file mode 100644
index 0000000..52328ef
Binary files /dev/null and b/kotlin-reporter-junit/img/properties.png differ
diff --git a/kotlin-reporter-junit/img/runReport.png b/kotlin-reporter-junit/img/runReport.png
new file mode 100644
index 0000000..ab2a27d
Binary files /dev/null and b/kotlin-reporter-junit/img/runReport.png differ
diff --git a/kotlin-reporter-junit/pom.xml b/kotlin-reporter-junit/pom.xml
new file mode 100644
index 0000000..9850be6
--- /dev/null
+++ b/kotlin-reporter-junit/pom.xml
@@ -0,0 +1,99 @@
+
+
+ 4.0.0
+
+ io.testomat
+ kotlin-reporter-junit
+ 1.0-SNAPSHOT
+
+
+ UTF-8
+ official
+ 1.8
+
+
+
+
+ mavenCentral
+ https://repo1.maven.org/maven2/
+
+
+
+
+ src/main/kotlin
+ src/test/kotlin
+
+
+ org.jetbrains.kotlin
+ kotlin-maven-plugin
+ 2.0.20
+
+
+ compile
+ compile
+
+ compile
+
+
+
+ test-compile
+ test-compile
+
+ test-compile
+
+
+
+
+
+ maven-surefire-plugin
+ 2.22.2
+
+
+ maven-failsafe-plugin
+ 2.22.2
+
+
+ org.codehaus.mojo
+ exec-maven-plugin
+ 1.6.0
+
+ MainKt
+
+
+
+
+
+
+
+ org.jetbrains.kotlin
+ kotlin-test-junit5
+ 2.0.20
+ test
+
+
+ io.rest-assured
+ rest-assured
+ 5.5.7
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter
+ 5.10.0
+ test
+
+
+ org.jetbrains.kotlin
+ kotlin-stdlib
+ 2.0.20
+
+
+ io.testomat
+ java-reporter-junit
+ 0.13.1
+
+
+
+
\ No newline at end of file
diff --git a/kotlin-reporter-junit/src/main/kotlin/Main.kt b/kotlin-reporter-junit/src/main/kotlin/Main.kt
new file mode 100644
index 0000000..9d3401c
--- /dev/null
+++ b/kotlin-reporter-junit/src/main/kotlin/Main.kt
@@ -0,0 +1,4 @@
+package io.testomat
+
+fun main() {
+}
\ No newline at end of file
diff --git a/kotlin-reporter-junit/src/main/resources/junit-platform.properties b/kotlin-reporter-junit/src/main/resources/junit-platform.properties
new file mode 100644
index 0000000..4dcbf9e
--- /dev/null
+++ b/kotlin-reporter-junit/src/main/resources/junit-platform.properties
@@ -0,0 +1,8 @@
+#This line is the mandatory
+junit.jupiter.extensions.autodetection.enabled = true
+
+junit.jupiter.execution.parallel.enabled=true
+junit.jupiter.execution.parallel.mode.default=concurrent
+junit.jupiter.execution.parallel.mode.classes.default=concurrent
+junit.jupiter.execution.parallel.config.strategy=dynamic
+junit.jupiter.execution.parallel.config.dynamic.factor=1.0
\ No newline at end of file
diff --git a/kotlin-reporter-junit/src/test/kotlin/tests/AdvancedApiTests.kt b/kotlin-reporter-junit/src/test/kotlin/tests/AdvancedApiTests.kt
new file mode 100644
index 0000000..28fe56e
--- /dev/null
+++ b/kotlin-reporter-junit/src/test/kotlin/tests/AdvancedApiTests.kt
@@ -0,0 +1,100 @@
+package tests
+
+import io.restassured.RestAssured.*
+import io.restassured.http.ContentType
+import org.hamcrest.Matchers.*
+import org.junit.jupiter.api.Test
+import java.util.concurrent.TimeUnit
+import io.testomat.core.annotation.TestId
+
+class AdvancedApiTests : BaseTest() {
+
+ @Test
+ @TestId("e2c71562")
+ fun `response should have expected headers`() {
+ get("/posts/1")
+ .then()
+ .statusCode(200)
+ .header("Content-Type", containsString("application/json"))
+ .header("X-Ratelimit-Limit", notNullValue())
+ .header("X-Ratelimit-Remaining", notNullValue())
+ }
+
+ @Test
+ @TestId("701867b2")
+ fun `response time should be acceptable`() {
+ get("/posts")
+ .then()
+ .statusCode(200)
+ .time(lessThan(5000L), TimeUnit.MILLISECONDS)
+ }
+
+ @Test
+ @TestId("5a80e045")
+ fun `extract and reuse response data`() {
+ val title: String = get("/posts/1")
+ .then()
+ .statusCode(200)
+ .extract()
+ .path("title")
+
+ val userId: Int = get("/posts/1")
+ .then()
+ .statusCode(200)
+ .extract()
+ .path("userId")
+
+ val titles: List = given()
+ .queryParam("userId", userId)
+ .`when`()
+ .get("/posts")
+ .then()
+ .statusCode(200)
+ .extract()
+ .path("title")
+
+ assert(titles.isNotEmpty())
+ assert(titles.all { it.isNotEmpty() })
+ }
+
+ @Test
+ @TestId("8f4f9513")
+ fun `response body as string and parse`() {
+ val body: String = get("/posts/1")
+ .then()
+ .statusCode(200)
+ .extract()
+ .asString()
+
+ assert(body.contains("sunt aut facere"))
+ assert(body.startsWith("{"))
+ }
+
+ @Test
+ @TestId("5c2433ea")
+ fun `get specific field from list response`() {
+ val names: List = get("/users")
+ .then()
+ .statusCode(200)
+ .extract()
+ .path("name")
+
+ assert(names.contains("Leanne Graham"))
+ assert(names.size == 10)
+ }
+
+ @Test
+ @TestId("564a873d")
+ fun `patch should work via override header`() {
+ val payload = """{"title":"patched-title"}"""
+ given()
+ .contentType(ContentType.JSON)
+ .header("X-HTTP-Method-Override", "PATCH")
+ .body(payload)
+ .`when`()
+ .post("/posts/1")
+ .then()
+ .statusCode(200)
+ .body("title", equalTo("patched-title"))
+ }
+}
diff --git a/kotlin-reporter-junit/src/test/kotlin/tests/AlbumsPhotosTests.kt b/kotlin-reporter-junit/src/test/kotlin/tests/AlbumsPhotosTests.kt
new file mode 100644
index 0000000..4ae4df9
--- /dev/null
+++ b/kotlin-reporter-junit/src/test/kotlin/tests/AlbumsPhotosTests.kt
@@ -0,0 +1,80 @@
+package tests
+
+import io.restassured.RestAssured.*
+import org.hamcrest.Matchers.*
+import org.junit.jupiter.api.Test
+import io.testomat.core.annotation.TestId
+
+class AlbumsPhotosTests : BaseTest() {
+
+ @Test
+ @TestId("baeaa69f")
+ fun `GET albums returns list`() {
+ get("/albums")
+ .then()
+ .statusCode(200)
+ .body("size()", greaterThan(0))
+ .body("[0].userId", notNullValue())
+ .body("[0].id", notNullValue())
+ .body("[0].title", notNullValue())
+ }
+
+ @Test
+ @TestId("992b5069")
+ fun `GET album by id`() {
+ get("/albums/1")
+ .then()
+ .statusCode(200)
+ .body("userId", equalTo(1))
+ .body("title", equalTo("quidem molestiae enim"))
+ }
+
+ @Test
+ @TestId("7e8fe52c")
+ fun `GET photos for album`() {
+ get("/albums/1/photos")
+ .then()
+ .statusCode(200)
+ .body("size()", greaterThan(0))
+ .body("[0].albumId", equalTo(1))
+ .body("[0].title", notNullValue())
+ .body("[0].url", notNullValue())
+ .body("[0].thumbnailUrl", notNullValue())
+ }
+
+ @Test
+ @TestId("b67876bb")
+ fun `GET photos endpoint`() {
+ get("/photos")
+ .then()
+ .statusCode(200)
+ .body("size()", greaterThan(0))
+ .body("[0].albumId", notNullValue())
+ .body("[0].url", containsString("https://"))
+ }
+
+ @Test
+ @TestId("779ccfcd")
+ fun `GET photo by id`() {
+ get("/photos/1")
+ .then()
+ .statusCode(200)
+ .body("id", equalTo(1))
+ .body("albumId", equalTo(1))
+ .body("title", notNullValue())
+ .body("url", notNullValue())
+ }
+
+ @Test
+ @TestId("581d506a")
+ fun `GET albums by userId`() {
+ given()
+ .queryParam("userId", 1)
+ .`when`()
+ .get("/albums")
+ .then()
+ .statusCode(200)
+ .body("size()", greaterThan(0))
+ .body("userId", everyItem(equalTo(1)))
+ }
+}
diff --git a/kotlin-reporter-junit/src/test/kotlin/tests/BaseTest.kt b/kotlin-reporter-junit/src/test/kotlin/tests/BaseTest.kt
new file mode 100644
index 0000000..dd18f91
--- /dev/null
+++ b/kotlin-reporter-junit/src/test/kotlin/tests/BaseTest.kt
@@ -0,0 +1,14 @@
+package tests
+
+import io.restassured.RestAssured.baseURI
+import org.junit.jupiter.api.BeforeAll
+import org.junit.jupiter.api.TestInstance
+
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+abstract class BaseTest {
+
+ @BeforeAll
+ fun setup() {
+ baseURI = "https://jsonplaceholder.typicode.com"
+ }
+}
diff --git a/kotlin-reporter-junit/src/test/kotlin/tests/CrudApiTests.kt b/kotlin-reporter-junit/src/test/kotlin/tests/CrudApiTests.kt
new file mode 100644
index 0000000..532b2d7
--- /dev/null
+++ b/kotlin-reporter-junit/src/test/kotlin/tests/CrudApiTests.kt
@@ -0,0 +1,105 @@
+package tests
+
+import io.restassured.RestAssured.*
+import io.restassured.http.ContentType
+import org.hamcrest.Matchers.*
+import org.junit.jupiter.api.Test
+import io.testomat.core.annotation.TestId
+
+class CrudApiTests : BaseTest() {
+
+ @Test
+ @TestId("c23988ce")
+ fun `GET posts returns list`() {
+ get("/posts")
+ .then()
+ .statusCode(200)
+ .contentType(ContentType.JSON)
+ .body("size()", greaterThan(0))
+ .body("[0].id", notNullValue())
+ .body("[0].title", notNullValue())
+ }
+
+ @Test
+ @TestId("05272ac5")
+ fun `GET post by id returns single object`() {
+ get("/posts/1")
+ .then()
+ .statusCode(200)
+ .body("id", equalTo(1))
+ .body("title", notNullValue())
+ .body("body", notNullValue())
+ .body("userId", notNullValue())
+ }
+
+ @Test
+ @TestId("973e6c28")
+ fun `POST creates a new resource`() {
+ val payload = """{"title":"foo","body":"bar","userId":1}"""
+ given()
+ .contentType(ContentType.JSON)
+ .body(payload)
+ .`when`()
+ .post("/posts")
+ .then()
+ .statusCode(201)
+ .body("title", equalTo("foo"))
+ .body("body", equalTo("bar"))
+ .body("id", notNullValue())
+ }
+
+ @Test
+ @TestId("87fd940a")
+ fun `PUT updates an existing resource`() {
+ val payload = """{"id":1,"title":"updated","body":"new body","userId":1}"""
+ given()
+ .contentType(ContentType.JSON)
+ .body(payload)
+ .`when`()
+ .put("/posts/1")
+ .then()
+ .statusCode(200)
+ .body("title", equalTo("updated"))
+ .body("body", equalTo("new body"))
+ }
+
+ @Test
+ @TestId("cb49c380")
+ fun `DELETE returns empty response`() {
+ delete("/posts/1")
+ .then()
+ .statusCode(200)
+ }
+
+ @Test
+ @TestId("a75e5a08")
+ fun `GET nonexistent resource returns 404`() {
+ get("/posts/99999")
+ .then()
+ .statusCode(404)
+ }
+
+ @Test
+ @TestId("43d91d1e")
+ fun `GET nested posts comments`() {
+ get("/posts/1/comments")
+ .then()
+ .statusCode(200)
+ .body("size()", greaterThan(0))
+ .body("[0].email", notNullValue())
+ .body("[0].name", notNullValue())
+ }
+
+ @Test
+ @TestId("1f4fcbb6")
+ fun `GET post with query param`() {
+ given()
+ .queryParam("postId", 1)
+ .`when`()
+ .get("/comments")
+ .then()
+ .statusCode(200)
+ .body("size()", greaterThan(0))
+ .body("postId", everyItem(equalTo(1)))
+ }
+}
diff --git a/kotlin-reporter-junit/src/test/kotlin/tests/ErrorScenariosTests.kt b/kotlin-reporter-junit/src/test/kotlin/tests/ErrorScenariosTests.kt
new file mode 100644
index 0000000..7719382
--- /dev/null
+++ b/kotlin-reporter-junit/src/test/kotlin/tests/ErrorScenariosTests.kt
@@ -0,0 +1,77 @@
+package tests
+
+import io.restassured.RestAssured.*
+import io.restassured.http.ContentType
+import org.hamcrest.Matchers.*
+import org.junit.jupiter.api.Test
+import io.testomat.core.annotation.TestId
+
+class ErrorScenariosTests : BaseTest() {
+
+ @Test
+ @TestId("4170370f")
+ fun `GET invalid endpoint returns 404`() {
+ get("/nonexistent")
+ .then()
+ .statusCode(404)
+ }
+
+ @Test
+ @TestId("a4d30df0")
+ fun `POST with empty body creates mock resource`() {
+ given()
+ .contentType(ContentType.JSON)
+ .body("{}")
+ .`when`()
+ .post("/posts")
+ .then()
+ .statusCode(201)
+ .body("id", notNullValue())
+ }
+
+ @Test
+ @TestId("043d0e66")
+ fun `POST does not require userId field`() {
+ given()
+ .contentType(ContentType.JSON)
+ .body("""{"title":"no-user"}""")
+ .`when`()
+ .post("/posts")
+ .then()
+ .statusCode(201)
+ .body("title", equalTo("no-user"))
+ .body("userId", nullValue())
+ }
+
+ @Test
+ @TestId("ca34f39b")
+ fun `PUT on nonexistent resource`() {
+ val payload = """{"title":"nowhere"}"""
+ given()
+ .contentType(ContentType.JSON)
+ .body(payload)
+ .`when`()
+ .put("/posts/99999")
+ .then()
+ .statusCode(500)
+ }
+
+ @Test
+ @TestId("b33e12b4")
+ fun `DELETE nonexistent resource`() {
+ delete("/posts/99999")
+ .then()
+ .statusCode(200)
+ }
+
+ @Test
+ @TestId("f2844d62")
+ fun `invalid query param types`() {
+ given()
+ .queryParam("id", "abc")
+ .`when`()
+ .get("/posts")
+ .then()
+ .statusCode(200)
+ }
+}
diff --git a/kotlin-reporter-junit/src/test/kotlin/tests/FilterApiTests.kt b/kotlin-reporter-junit/src/test/kotlin/tests/FilterApiTests.kt
new file mode 100644
index 0000000..75b3063
--- /dev/null
+++ b/kotlin-reporter-junit/src/test/kotlin/tests/FilterApiTests.kt
@@ -0,0 +1,86 @@
+package tests
+
+import io.restassured.RestAssured.*
+import org.hamcrest.Matchers.*
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.ValueSource
+import io.testomat.core.annotation.TestId
+
+class FilterApiTests : BaseTest() {
+
+ @Test
+ @TestId("2f5f0714")
+ fun `GET comments by postId filter`() {
+ given()
+ .queryParam("postId", 1)
+ .`when`()
+ .get("/comments")
+ .then()
+ .statusCode(200)
+ .body("size()", greaterThan(0))
+ .body("postId", everyItem(equalTo(1)))
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = ["/posts/1/comments", "/comments?postId=1"])
+ @TestId("e9f201c0")
+ fun `comments for post 1 returned by both endpoints`(path: String) {
+ get(path)
+ .then()
+ .statusCode(200)
+ .body("size()", greaterThan(0))
+ .body("[0].postId", equalTo(1))
+ }
+
+ @Test
+ @TestId("d7cef041")
+ fun `GET limited results via query params`() {
+ given()
+ .queryParam("userId", 1)
+ .queryParam("completed", true)
+ .`when`()
+ .get("/todos")
+ .then()
+ .statusCode(200)
+ .body("size()", greaterThan(0))
+ .body("userId", everyItem(equalTo(1)))
+ .body("completed", everyItem(equalTo(true)))
+ }
+
+ @Test
+ @TestId("5483a8cd")
+ fun `GET all todos`() {
+ get("/todos")
+ .then()
+ .statusCode(200)
+ .body("size()", greaterThan(0))
+ .body("[0].userId", notNullValue())
+ .body("[0].title", notNullValue())
+ .body("[0].completed", notNullValue())
+ }
+
+ @Test
+ @TestId("b4820150")
+ fun `GET users endpoint returns collection`() {
+ get("/users")
+ .then()
+ .statusCode(200)
+ .body("size()", equalTo(10))
+ .body("[0].name", equalTo("Leanne Graham"))
+ .body("[0].email", equalTo("Sincere@april.biz"))
+ }
+
+ @Test
+ @TestId("c82a81f9")
+ fun `GET todos by userId returns matching items`() {
+ given()
+ .queryParam("userId", 1)
+ .`when`()
+ .get("/todos")
+ .then()
+ .statusCode(200)
+ .body("size()", greaterThan(0))
+ .body("userId", everyItem(equalTo(1)))
+ }
+}
diff --git a/kotlin-reporter-junit/src/test/kotlin/tests/ParameterizedApiTests.kt b/kotlin-reporter-junit/src/test/kotlin/tests/ParameterizedApiTests.kt
new file mode 100644
index 0000000..1bdb3e1
--- /dev/null
+++ b/kotlin-reporter-junit/src/test/kotlin/tests/ParameterizedApiTests.kt
@@ -0,0 +1,54 @@
+package tests
+
+import io.restassured.RestAssured.*
+import org.hamcrest.Matchers.*
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.*
+import io.testomat.core.annotation.TestId
+
+class ParameterizedApiTests : BaseTest() {
+
+ @ParameterizedTest
+ @CsvSource(
+ "1, Leanne Graham, Sincere@april.biz",
+ "2, Ervin Howell, Shanna@melissa.tv",
+ "3, Clementine Bauch, Nathan@yesenia.net"
+ )
+ @TestId("c1ba7a90")
+ fun `GET user by id returns expected name and email`(id: Int, name: String, email: String) {
+ get("/users/$id")
+ .then()
+ .statusCode(200)
+ .body("name", equalTo(name))
+ .body("email", equalTo(email))
+ }
+
+ @ParameterizedTest
+ @ValueSource(ints = [1, 2, 3, 5, 10])
+ @TestId("688a9b2d")
+ fun `GET post by id returns 200 for valid ids`(id: Int) {
+ get("/posts/$id")
+ .then()
+ .statusCode(200)
+ .body("id", equalTo(id))
+ }
+
+ @ParameterizedTest
+ @MethodSource("postIdsWithTitles")
+ @TestId("04ad8cce")
+ fun `GET post by id returns expected title`(id: Int, title: String) {
+ get("/posts/$id")
+ .then()
+ .statusCode(200)
+ .body("title", equalTo(title))
+ }
+
+ companion object {
+ @JvmStatic
+ fun postIdsWithTitles() = listOf(
+ Arguments.of(1, "sunt aut facere repellat provident occaecati excepturi optio reprehenderit"),
+ Arguments.of(2, "qui est esse"),
+ Arguments.of(3, "ea molestias quasi exercitationem repellat qui ipsa sit aut")
+ )
+ }
+}
diff --git a/kotlin-reporter-junit/src/test/kotlin/tests/UserApiTests.kt b/kotlin-reporter-junit/src/test/kotlin/tests/UserApiTests.kt
new file mode 100644
index 0000000..833cef9
--- /dev/null
+++ b/kotlin-reporter-junit/src/test/kotlin/tests/UserApiTests.kt
@@ -0,0 +1,66 @@
+package tests
+
+import io.restassured.RestAssured.*
+import org.hamcrest.Matchers.*
+import org.junit.jupiter.api.Test
+import io.testomat.core.annotation.TestId
+
+class UserApiTests : BaseTest() {
+
+ companion object {
+ private const val USERS = "/users"
+ private const val USER_1 = "/users/1"
+ }
+
+ private fun getUser1() =
+ get(USER_1)
+ .then()
+ .statusCode(200)
+
+ @Test
+ @TestId("7f6a9557")
+ fun `GET users returns list of 10 users`() {
+ get(USERS)
+ .then()
+ .statusCode(200)
+ .body("size()", equalTo(10))
+ }
+
+ @Test
+ @TestId("31fe6da1")
+ fun `GET user by id returns correct structure`() {
+ getUser1()
+ .body("name", equalTo("Leanne Graham"))
+ .body("username", equalTo("Bret"))
+ .body("email", equalTo("Sincere@april.biz"))
+ .body("phone", notNullValue())
+ .body("website", notNullValue())
+ }
+
+ @Test
+ @TestId("52ef376a")
+ fun `user has nested address object`() {
+ getUser1()
+ .body("address.street", equalTo("Kulas Light"))
+ .body("address.suite", equalTo("Apt. 556"))
+ .body("address.city", equalTo("Gwenborough"))
+ .body("address.zipcode", equalTo("92998-3874"))
+ }
+
+ @Test
+ @TestId("fe373ddf")
+ fun `user has company info`() {
+ getUser1()
+ .body("company.name", equalTo("Romaguera-Crona"))
+ .body("company.catchPhrase", notNullValue())
+ .body("company.bs", notNullValue())
+ }
+
+ @Test
+ @TestId("58de887c")
+ fun `user has geo location`() {
+ getUser1()
+ .body("address.geo.lat", notNullValue())
+ .body("address.geo.lng", notNullValue())
+ }
+}
\ No newline at end of file