feature: #6: Add cucumber test for article routes
This commit is contained in:
9
.idea/compiler.xml
generated
9
.idea/compiler.xml
generated
@@ -1,9 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="CompilerConfiguration">
|
|
||||||
<bytecodeTargetLevel>
|
|
||||||
<module name="dcproject.main" target="11" />
|
|
||||||
<module name="dcproject.test" target="11" />
|
|
||||||
</bytecodeTargetLevel>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
1
.idea/misc.xml
generated
1
.idea/misc.xml
generated
@@ -1,5 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
|
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="11" project-jdk-type="JavaSDK">
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="11" project-jdk-type="JavaSDK">
|
||||||
<output url="file://$PROJECT_DIR$/out" />
|
<output url="file://$PROJECT_DIR$/out" />
|
||||||
</component>
|
</component>
|
||||||
|
|||||||
@@ -38,6 +38,14 @@ dependencies {
|
|||||||
implementation("net.pearx.kasechange:kasechange-jvm:1.1.0")
|
implementation("net.pearx.kasechange:kasechange-jvm:1.1.0")
|
||||||
implementation("fr.postgresjson:postgresjson:$postgresjson_version")
|
implementation("fr.postgresjson:postgresjson:$postgresjson_version")
|
||||||
testImplementation("io.ktor:ktor-server-tests:$ktor_version")
|
testImplementation("io.ktor:ktor-server-tests:$ktor_version")
|
||||||
|
testImplementation("io.ktor:ktor-client-mock:$ktor_version")
|
||||||
|
testImplementation("io.ktor:ktor-client-mock-jvm:$ktor_version")
|
||||||
|
testImplementation("org.koin:koin-test:$koinVersion")
|
||||||
|
testImplementation("io.mockk:mockk:1.9")
|
||||||
|
testImplementation("org.junit.jupiter:junit-jupiter:5.5.0")
|
||||||
|
testImplementation("org.amshove.kluent:kluent:1.4")
|
||||||
|
testImplementation("io.cucumber:cucumber-java8:4.3.1")
|
||||||
|
testImplementation("io.cucumber:cucumber-junit:4.3.1")
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlin.sourceSets["main"].kotlin.srcDirs("src")
|
kotlin.sourceSets["main"].kotlin.srcDirs("src")
|
||||||
|
|||||||
@@ -6,9 +6,10 @@ import org.koin.dsl.module
|
|||||||
import java.io.File
|
import java.io.File
|
||||||
import fr.dcproject.repository.Article as ArticleRepository
|
import fr.dcproject.repository.Article as ArticleRepository
|
||||||
|
|
||||||
|
val config = Config()
|
||||||
|
|
||||||
@KtorExperimentalAPI
|
@KtorExperimentalAPI
|
||||||
val Module = module {
|
val Module = module {
|
||||||
val config = Config()
|
|
||||||
|
|
||||||
single { config }
|
single { config }
|
||||||
|
|
||||||
|
|||||||
@@ -21,10 +21,17 @@ class Article(override var requester: Requester) : RepositoryI<ArticleEntity> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun find(page: Int = 1, limit: Int = 50, sort: String? = null, direction: Direction? = null, search: String? = null): Paginated<ArticleEntity> {
|
fun find(
|
||||||
|
page: Int = 1,
|
||||||
|
limit: Int = 50,
|
||||||
|
sort: String? = null,
|
||||||
|
direction: Direction? = null,
|
||||||
|
search: String? = null
|
||||||
|
): Paginated<ArticleEntity> {
|
||||||
return requester
|
return requester
|
||||||
.getFunction("find_articles")
|
.getFunction("find_articles")
|
||||||
.select(page, limit,
|
.select(
|
||||||
|
page, limit,
|
||||||
"sort" to sort?.toSnakeCase(),
|
"sort" to sort?.toSnakeCase(),
|
||||||
"direction" to direction,
|
"direction" to direction,
|
||||||
"search" to search
|
"search" to search
|
||||||
|
|||||||
25
test/RunCucumberTest.kt
Normal file
25
test/RunCucumberTest.kt
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import cucumber.api.CucumberOptions
|
||||||
|
import cucumber.api.Scenario
|
||||||
|
import cucumber.api.java8.En
|
||||||
|
import cucumber.api.junit.Cucumber
|
||||||
|
import feature.Context
|
||||||
|
import io.ktor.server.testing.TestApplicationEngine
|
||||||
|
import io.ktor.server.testing.createTestEnvironment
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import feature.Context.Companion.current as contextCurrent
|
||||||
|
|
||||||
|
@RunWith(Cucumber::class)
|
||||||
|
@CucumberOptions(plugin = ["pretty"])
|
||||||
|
class RunCucumberTest: En {
|
||||||
|
init {
|
||||||
|
Before(-1) { scenario: Scenario ->
|
||||||
|
// config.database = "dc-projectg-test"
|
||||||
|
contextCurrent = Context(TestApplicationEngine(createTestEnvironment()) {}, scenario)
|
||||||
|
}
|
||||||
|
|
||||||
|
After { scenario: Scenario ->
|
||||||
|
contextCurrent.engine.stop(0L, 0L, TimeUnit.MILLISECONDS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
40
test/feature/Context.kt
Normal file
40
test/feature/Context.kt
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
package feature
|
||||||
|
|
||||||
|
import cucumber.api.Scenario
|
||||||
|
import fr.dcproject.module
|
||||||
|
import io.ktor.application.Application
|
||||||
|
import io.ktor.server.testing.TestApplicationCall
|
||||||
|
import io.ktor.server.testing.TestApplicationEngine
|
||||||
|
import io.ktor.server.testing.TestApplicationRequest
|
||||||
|
|
||||||
|
class Context(
|
||||||
|
val engine: TestApplicationEngine,
|
||||||
|
val scenario: Scenario
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
lateinit var current: Context
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
engine.start()
|
||||||
|
val moduleFunction: Application.() -> Unit = { module() }
|
||||||
|
val test: TestApplicationEngine.() -> Unit = {
|
||||||
|
moduleFunction(application)
|
||||||
|
}
|
||||||
|
engine.test()
|
||||||
|
}
|
||||||
|
|
||||||
|
var call: TestApplicationCall? = null
|
||||||
|
|
||||||
|
private val requestContextConfigurations: MutableList<TestApplicationRequest.() -> Unit> = mutableListOf()
|
||||||
|
fun setupRequest(testApplicationRequest: TestApplicationRequest) {
|
||||||
|
requestContextConfigurations.forEach {
|
||||||
|
it(testApplicationRequest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun setupNextRequests(requestContextConfiguration: TestApplicationRequest.() -> Unit) = requestContextConfigurations.add(requestContextConfiguration)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun TestApplicationRequest.applyConfigurations() {
|
||||||
|
Context.current.setupRequest(this)
|
||||||
|
}
|
||||||
92
test/feature/Request.kt
Normal file
92
test/feature/Request.kt
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
package feature
|
||||||
|
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import cucumber.api.Scenario
|
||||||
|
import cucumber.api.java8.En
|
||||||
|
import io.cucumber.datatable.DataTable
|
||||||
|
import io.ktor.http.ContentType
|
||||||
|
import io.ktor.http.HttpHeaders
|
||||||
|
import io.ktor.http.HttpMethod
|
||||||
|
import io.ktor.http.HttpStatusCode
|
||||||
|
import io.ktor.server.testing.TestApplicationCall
|
||||||
|
import io.ktor.server.testing.TestApplicationEngine
|
||||||
|
import io.ktor.server.testing.setBody
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.koin.test.KoinTest
|
||||||
|
import org.opentest4j.AssertionFailedError
|
||||||
|
import kotlin.test.asserter
|
||||||
|
import feature.Context.Companion.current as currentContext
|
||||||
|
|
||||||
|
class Request: En, KoinTest {
|
||||||
|
init {
|
||||||
|
Before { scenario: Scenario ->
|
||||||
|
}
|
||||||
|
|
||||||
|
After { scenario: Scenario ->
|
||||||
|
}
|
||||||
|
|
||||||
|
When("I send a {string} request to {string} with body:") { method: String, uri: String, body: String ->
|
||||||
|
val test: TestApplicationEngine.() -> Unit = {
|
||||||
|
currentContext.call = handleRequest {
|
||||||
|
applyConfigurations()
|
||||||
|
addHeader(HttpHeaders.ContentType, ContentType.Application.Json.toString())
|
||||||
|
this.method = HttpMethod.parse(method)
|
||||||
|
this.uri = uri
|
||||||
|
setBody(body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
currentContext.engine.test()
|
||||||
|
}
|
||||||
|
|
||||||
|
When("I send a {string} request to {string}") { method: String, uri: String ->
|
||||||
|
val test: TestApplicationEngine.() -> Unit = {
|
||||||
|
currentContext.call = handleRequest {
|
||||||
|
applyConfigurations()
|
||||||
|
addHeader(HttpHeaders.ContentType, ContentType.Application.Json.toString())
|
||||||
|
this.method = HttpMethod.parse(method.toUpperCase())
|
||||||
|
this.uri = uri
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
currentContext.engine.test()
|
||||||
|
}
|
||||||
|
|
||||||
|
Then("the response status code should be {int}") { statusCode: Int ->
|
||||||
|
val call: TestApplicationCall = currentContext.call ?: throw AssertionFailedError("No call", statusCode, null)
|
||||||
|
with(call) {
|
||||||
|
assertEquals(HttpStatusCode.fromValue(statusCode), response.status())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
And("the response should contain:") { expected: DataTable ->
|
||||||
|
val call: TestApplicationCall = currentContext.call ?: throw AssertionFailedError("No call")
|
||||||
|
val p = call.response
|
||||||
|
val response = Gson().fromJson<List<Map<String, String>>>(p.content, List::class.java)
|
||||||
|
|
||||||
|
expected.asMap<String, String>(String::class.java, String::class.java).forEach { (key, value) ->
|
||||||
|
response.forEach {
|
||||||
|
if (it.containsKey(key)) {
|
||||||
|
assertEquals(it[key], value)
|
||||||
|
return@And
|
||||||
|
}
|
||||||
|
}
|
||||||
|
asserter.fail("The response not contain $key field")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
And("the response should contain object:") { expected: DataTable ->
|
||||||
|
val call: TestApplicationCall = currentContext.call ?: throw AssertionFailedError("No call")
|
||||||
|
val p = call.response
|
||||||
|
val response = Gson().fromJson<Map<String, String>>(p.content, Map::class.java)
|
||||||
|
|
||||||
|
expected.asMap<String, String>(String::class.java, String::class.java).forEach { (key, value) ->
|
||||||
|
if (response.containsKey(key)) {
|
||||||
|
assertEquals(value, response[key])
|
||||||
|
return@And
|
||||||
|
}
|
||||||
|
asserter.fail("The response not contain $key field")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
36
testresources/feature/articles.feature
Normal file
36
testresources/feature/articles.feature
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
Feature: articles routes
|
||||||
|
|
||||||
|
Scenario: The route for get articles must response a 200
|
||||||
|
When I send a "GET" request to "/articles"
|
||||||
|
Then the response status code should be 200
|
||||||
|
|
||||||
|
Scenario: The route for get article must response a 200
|
||||||
|
When I send a "GET" request to "/articles/55a24426-139b-4ee7-b1e2-a3d016d66cc2"
|
||||||
|
Then the response status code should be 200
|
||||||
|
|
||||||
|
Scenario: The route for get article must response a 200
|
||||||
|
When I send a "POST" request to "/articles" with body:
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"version_id": "09c418b6-63ba-448b-b38b-502b41cd500e",
|
||||||
|
"title": "title2",
|
||||||
|
"annonymous": false,
|
||||||
|
"content": "content2",
|
||||||
|
"description": "description2",
|
||||||
|
"tags": [
|
||||||
|
"green"
|
||||||
|
],
|
||||||
|
"created_by": {
|
||||||
|
"id": "64b7b379-2298-43ec-b428-ba134930cabd"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
Then the response status code should be 200
|
||||||
|
And the response should contain object:
|
||||||
|
| version_id | 09c418b6-63ba-448b-b38b-502b41cd500e |
|
||||||
|
| title | title2 |
|
||||||
|
When I send a "GET" request to "/articles/99afd1b1-3555-43c1-80a7-63c56e93d250"
|
||||||
|
Then the response status code should be 200
|
||||||
|
And the response should contain object:
|
||||||
|
| id | 99afd1b1-3555-43c1-80a7-63c56e93d250 |
|
||||||
|
| title | title2 |
|
||||||
Reference in New Issue
Block a user