Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[testing] ContextMock (#1976) #2038

Merged
merged 38 commits into from Dec 6, 2023
Merged
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
6839d38
[testing] `ContextMock` concept v1
dzikoysk Oct 9, 2023
4082cb3
Merge branch 'main' into gh-1976/in-memory-res-req
dzikoysk Oct 9, 2023
709bf65
[testing] `ContextMock` concept v2 - Copy mock on cfg change
dzikoysk Oct 9, 2023
f9d6294
[testing] `ContextMock` concept v3 - Handle request using a real Java…
dzikoysk Oct 9, 2023
0730603
[testing] Fix Javalin init order
dzikoysk Oct 9, 2023
f715eb0
Merge branch 'main' into gh-1976/in-memory-res-req
dzikoysk Oct 23, 2023
2b3ada7
[testing] Create `Endpoint` API
dzikoysk Oct 23, 2023
6ec11eb
[testing] Replace multiple config methods with one `withMockConfig`
dzikoysk Oct 24, 2023
8e99602
[testing] Hide mocking from Endpoint
dzikoysk Oct 24, 2023
d5ce12d
[testing] Replace `Consumer<MockConfig>` with SAM-with-receiver inter…
dzikoysk Oct 25, 2023
2c3d949
[testing] Refactor mocked Body impl & tests
dzikoysk Oct 25, 2023
e26838e
[testing] Cleanup + docs
dzikoysk Oct 25, 2023
a759b1b
Merge branch 'main' into gh-1976/in-memory-res-req
dzikoysk Nov 1, 2023
5989b03
[testing] Support sessions
dzikoysk Nov 1, 2023
53f8e31
[testing] Compare state of mocked ctx (& res/res) with real ctx
dzikoysk Nov 1, 2023
03d2128
[testing] Fix test
dzikoysk Nov 1, 2023
8d6e7bf
[testing] Cleanup names
dzikoysk Nov 1, 2023
b24df3c
[testing] Delete javalin-context-mock related classes
dzikoysk Nov 3, 2023
4e38eea
[testing] `javalin-util`/`javalin-context-mock` module with `ContextM…
dzikoysk Nov 4, 2023
429b0a5
[testing] Fix javalin-util pom.xml
dzikoysk Nov 4, 2023
b0a1a26
[testing] Add `javalin-context-mock` to `javalin-bundle`
dzikoysk Nov 4, 2023
de9472b
Merge branch 'main' into gh-1976/in-memory-res-req
dzikoysk Nov 4, 2023
da72c00
Merge branch 'gh-1976/in-memory-res-req' into gh-1976/context-mock
dzikoysk Nov 4, 2023
c76fb57
[testing] Duplicate `pom.properties` for `javalin-utils` and its subm…
dzikoysk Nov 4, 2023
5a7d461
[testing] Support multipart files
dzikoysk Nov 4, 2023
3802574
[testing] Bootstrap other session methods
dzikoysk Nov 4, 2023
f45e6f2
[testing] Test headers in the response
dzikoysk Nov 4, 2023
659aff1
Merge branch 'main' into gh-1976/in-memory-res-req
dzikoysk Nov 15, 2023
d29e626
Cleanup
dzikoysk Nov 15, 2023
50a0e61
Merge branch 'gh-1976/in-memory-res-req' into gh-1976/context-mock
dzikoysk Nov 16, 2023
993dbc8
Cleanup
dzikoysk Nov 16, 2023
0cc4769
Merge branch 'main' into gh-1976/context-mock
dzikoysk Nov 20, 2023
bcfe488
Merge branch 'master' into gh-1976/context-mock
dzikoysk Dec 1, 2023
86c80e1
Update javalin-utils/javalin-context-mock/src/main/java/io/javalin/mo…
dzikoysk Dec 4, 2023
fa67912
Merge branch 'master' into gh-1976/context-mock
dzikoysk Dec 4, 2023
24ff374
[testing] CR v1
dzikoysk Dec 4, 2023
d2300b1
[testing] CR v2: replace pom.properties content with dummy comment
dzikoysk Dec 5, 2023
80c7597
[testing] CR v3: rename invokeMockConfigurerWithAsSamWithReceiver to …
dzikoysk Dec 5, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions javalin-bundle/pom.xml
Expand Up @@ -19,6 +19,11 @@
<artifactId>javalin</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.javalin</groupId>
<artifactId>javalin-context-mock</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.javalin</groupId>
<artifactId>javalin-testtools</artifactId>
Expand Down
85 changes: 85 additions & 0 deletions javalin-utils/javalin-context-mock/pom.xml
@@ -0,0 +1,85 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>javalin-utils</artifactId>
<groupId>io.javalin</groupId>
<version>6.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>javalin-context-mock</artifactId>

<properties>
<copy-build-resources.skip>false</copy-build-resources.skip>
</properties>

<dependencies>
<dependency>
<groupId>io.javalin</groupId>
<artifactId>javalin</artifactId>
<version>${project.version}</version>
<!-- We have to exclude Jetty here, but include it as a test dependency -->
<exclusions>
<exclusion>
<groupId>org.eclipse.jetty</groupId>
<artifactId>*</artifactId>
</exclusion>
<exclusion>
<groupId>org.eclipse.jetty.websocket</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.toolchain</groupId>
<artifactId>jetty-jakarta-servlet-api</artifactId>
<version>${jetty.jakarta-api}</version>
</dependency>

<!-- BEGIN TEST DEPENDENCIES -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>${assertj.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-servlet</artifactId>
<version>${jetty.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.websocket</groupId>
<artifactId>websocket-jetty-server</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-kotlin</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.konghq</groupId>
<artifactId>unirest-java</artifactId>
<version>${unirest.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,64 @@
package io.javalin.mock

import io.javalin.http.ContentType
import io.javalin.json.JsonMapper
import io.javalin.json.toJsonString
import java.io.InputStream

interface Body {

fun init(mockConfig: ContextMockConfig) {}
fun toInputStream(): InputStream
fun getContentType(): String?
fun getContentLength(): Long?

companion object {

private abstract class AbstractBody(private val contentType: String?, private val contentLength: Long? = null) : Body {
override fun getContentType(): String? = contentType
override fun getContentLength(): Long? = contentLength
}

@JvmStatic
@JvmOverloads
fun ofString(
body: String,
contentType: String? = ContentType.PLAIN,
contentLength: Long? = body.length.toLong()
): Body =
object : AbstractBody(contentType = contentType, contentLength = contentLength) {
override fun toInputStream(): InputStream = body.byteInputStream()
}

@JvmStatic
@JvmOverloads
fun ofInputStream(
body: InputStream,
contentType: String? = ContentType.OCTET_STREAM,
contentLength: Long? = null
): Body =
object : AbstractBody(contentType = contentType) {
val content = body.use { it.readAllBytes() }
override fun toInputStream(): InputStream = content.inputStream()
override fun getContentLength(): Long = contentLength ?: content.size.toLong()
}

@JvmStatic
@JvmOverloads
fun ofObject(
body: Any,
contentType: String? = ContentType.JSON,
contentLength: Long? = null
): Body =
object : AbstractBody(contentType = contentType, contentLength = contentLength) {
private lateinit var content: ByteArray
override fun init(mockConfig: ContextMockConfig) {
this.content = mockConfig.javalinConfig.pvt.jsonMapper!!.toJsonString(body).toByteArray()
}
override fun toInputStream(): InputStream = content.inputStream()
override fun getContentLength(): Long = content.size.toLong()
}

}

}
@@ -0,0 +1,138 @@
package io.javalin.mock

import io.javalin.http.Context
import io.javalin.http.Header
import io.javalin.http.servlet.JavalinServlet
import io.javalin.http.servlet.JavalinServletContext
import io.javalin.http.servlet.JavalinServletContextConfig
import io.javalin.http.servlet.SubmitOrder.LAST
import io.javalin.http.servlet.Task
import io.javalin.http.servlet.TaskInitializer
import io.javalin.router.Endpoint
import io.javalin.router.EndpointExecutor
import io.javalin.mock.servlet.HttpServletRequestMock
import io.javalin.mock.servlet.HttpServletResponseMock
import java.util.concurrent.CountDownLatch
import java.util.function.Consumer
import org.jetbrains.annotations.ApiStatus.Experimental

/**
* A [ContextMock] is an in-memory [Context] instance builder.
*
* Although this implementation can be used in different ways, the most common use case is to build a [Context] instance within the test scope.
* We're strongly recommending to use [ContextMock] over any other reflection based mocking library, as it's way closer to the real implementation.
dzikoysk marked this conversation as resolved.
Show resolved Hide resolved
*
* By default, the request state represents an incoming connection from localhost to the root path.
* Javalin configuration, request and response states can be modified by using the [ContextMockConfigurer] interface.
* Once the state is prepared, you can build a [Context] instance in two ways:
* - [ContextMock.build] with [Endpoint] instance: to simulate a real request/response cycle (recommended)
* - [ContextMock.execute]: to execute non-endpoint related code that requires a [Context] instance
*
* See docs for more information: https://javalin.io/documentation#context-mock
*/
@Experimental
class ContextMock private constructor(
private val mockConfig: ContextMockConfig = ContextMockConfig(),
private val userConfigs: List<ContextMockConfigurer> = emptyList(),
) : EndpointExecutor {

companion object {
@JvmStatic
@JvmOverloads
fun create(configurer: ContextMockConfigurer? = null): ContextMock =
ContextMock(
userConfigs = configurer?.let { listOf(it) } ?: emptyList()
)
}

/** Register additional [ContextMockConfigurer]s. Each [ContextMock] can have multiple configurers, which are applied in registration order. */
fun withMockConfig(cfg: ContextMockConfigurer): ContextMock =
ContextMock(
mockConfig = mockConfig.clone(),
userConfigs = userConfigs + cfg
)

/** Build an [EndpointExecutor] with a [uri], [Body] or/and a [configurer]. */
@JvmOverloads
fun build(uri: String? = null, body: Body? = null, configurer: ContextMockConfigurer? = null): EndpointExecutor =
EndpointExecutor { endpoint ->
execute(endpoint, uri ?: endpoint.path, body, configurer)
}

/** Build an [EndpointExecutor] with a [Body]. */
fun build(body: Body? = null, configurer: ContextMockConfigurer? = null): EndpointExecutor =
build(null, body, configurer)

/** Execute a non-endpoint related code that requires [Context] instance **/
fun execute(body: Consumer<Context>): Context {
val (req, res) = createMockReqAndRes()
val ctx = JavalinServletContext(createServletContextConfig(), req = req, res = res)
body.accept(ctx)
return ctx
}

/** Execute this ContextMock without any additional parameters **/
override fun execute(endpoint: Endpoint): Context {
return build().execute(endpoint)
}

private fun execute(endpoint: Endpoint, uri: String = endpoint.path, body: Body? = null, configurer: ContextMockConfigurer? = null): Context {
// create req & res using standard configurers
val (request, response) = createMockReqAndRes()
// apply provided body to the request
body?.init(mockConfig)
// apply defaults values
mockConfig.req.also { req ->
req.method = endpoint.method.name
req.contextPath = mockConfig.javalinConfig.router.contextPath.takeIf { it != "/" } ?: ""
req.requestURI = uri
req.requestURL = "${req.scheme}://${req.serverName}:${req.serverPort}${req.contextPath}${req.requestURI}"
req.inputStream = body?.toInputStream() ?: req.inputStream
req.contentType = body?.getContentType() ?: req.contentType
req.contentLength = body?.getContentLength() ?: req.contentLength
}
// run final request configurer for this particular request
configurer?.let { invokeMockConfigurerWithAsSamWithReceiver(it, mockConfig) }
// synchronize request state with headers
mockConfig.req.also { req ->
req.headers.computeIfAbsent(Header.CONNECTION) { mutableListOf("keep-alive") }
req.headers.computeIfAbsent(Header.HOST) { mutableListOf("localhost:${req.serverPort}") }
req.headers.computeIfAbsent(Header.USER_AGENT) { mutableListOf("javalin-mock/1.0") }
req.headers.computeIfAbsent(Header.ACCEPT_ENCODING) { mockConfig.javalinConfig.pvt.compressionStrategy.compressors.mapTo(ArrayList()) { it.encoding() } }
req.headers.computeIfAbsent(Header.CONTENT_TYPE) { req.contentType?.let { mutableListOf(it) } ?: mutableListOf() }
req.headers.computeIfAbsent(Header.CONTENT_LENGTH) { req.contentLength.takeIf { it > 0 }?.let { mutableListOf(it.toString()) } ?: mutableListOf() }
}

val await = CountDownLatch(1)
val javalinServlet = JavalinServlet(mockConfig.javalinConfig)
(javalinServlet.requestLifecycle as MutableList<TaskInitializer<JavalinServletContext>>).add(
TaskInitializer { submitTask, _, _, _ ->
submitTask(LAST, Task(skipIfExceptionOccurred = false) {
await.countDown()
})
}
)
javalinServlet.router.addHttpEndpoint(endpoint)
val ctx = javalinServlet.handle(request, response)!!
await.await()

return ctx
}

private fun createMockReqAndRes(): Pair<HttpServletRequestMock, HttpServletResponseMock> {
userConfigs.forEach { invokeMockConfigurerWithAsSamWithReceiver(it, mockConfig) }
val response = HttpServletResponseMock(mockConfig.res)
val request = HttpServletRequestMock(mockConfig.req, response)
return request to response
}

private fun createServletContextConfig(): JavalinServletContextConfig =
JavalinServletContextConfig(
appAttributes = mockConfig.javalinConfig.pvt.appAttributes,
compressionStrategy = mockConfig.javalinConfig.pvt.compressionStrategy,
defaultContentType = mockConfig.javalinConfig.http.defaultContentType,
jsonMapper = mockConfig.javalinConfig.pvt.jsonMapper!!,
requestLoggerEnabled = false,
)

}
@@ -0,0 +1,28 @@
package io.javalin.mock

import io.javalin.Javalin
import io.javalin.config.JavalinConfig
import io.javalin.mock.servlet.HttpServletRequestMock.RequestState
import io.javalin.mock.servlet.HttpServletResponseMock.ResponseState
import java.util.function.Consumer

data class ContextMockConfig internal constructor(
val req: RequestState = RequestState(),
val res: ResponseState = ResponseState(),
var javalinConfig: JavalinConfig = Javalin.create().unsafeConfig()
) {

/** Change Javalin config used to prepare the [Context] instance. */
fun javalinConfig(config: Consumer<JavalinConfig>) {
this.javalinConfig = Javalin.create(config).unsafeConfig()
}

/** Deep copy of this [ContextMockConfig] */
fun clone(): ContextMockConfig =
copy(
req = req.copy(),
res = res.copy(),
javalinConfig = javalinConfig // TODO: we could clone this too (?)
tipsy marked this conversation as resolved.
Show resolved Hide resolved
)

}
@@ -0,0 +1,10 @@
package io.javalin.mock

fun interface ContextMockConfigurer {
/** Apply changes to the [ContextMockConfig] instance. */
fun ContextMockConfig.configure()
}

internal fun invokeMockConfigurerWithAsSamWithReceiver(fn: ContextMockConfigurer, receiver: ContextMockConfig) {
with(fn) { receiver.configure() }
}
dzikoysk marked this conversation as resolved.
Show resolved Hide resolved