-
Notifications
You must be signed in to change notification settings - Fork 38k
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
Add Kotlin DSL for working with MockMvc #1951
Conversation
Can I get any feedback in regards to this PR? No rush, just curious if I should improve parts. |
Thanks for this pull request, please find bellow my proposal inspired from yours: @Test
fun json() {
mockMvc.perform(HttpMethod.GET, "/person/{name}", "Lee") {
accept = MediaType.APPLICATION_JSON
print()
expect {
status.isOk()
content.contentType("application/json;charset=UTF-8")
jsonPath("\$.name").value("Lee")
json("""{"someBoolean": false}""", strict = false)
}
}
} Any thoughts? |
Thanks for the reply! I like it. The main differences I see:
So I'll begin researching how difficult points 1 and 4 are to accomplish. I want to say there were technical limitations that made the original approach needed, but I'll investigate to be certain. |
@sdeleuze One discoverability caveat we ran into is when using |
|
|
For |
I've pushed updates to incorporate your comments. Here is its current state: @Test
fun json() {
mockMvc.perform(HttpMethod.GET,"/person/{name}", "Lee") {
builder { accept(MediaType.APPLICATION_JSON) }
print()
expect {
status { isOk }
content { contentType("application/json;charset=UTF-8") }
jsonPath("$.name") { value("Lee") }
json("""{"someBoolean": false}""", strict = false)
}
}
} I've had to keep the |
} | ||
|
||
@Test | ||
fun json() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here is the passing test that demonstrates the DSL
builder { accept(MediaType.APPLICATION_XML) } | ||
|
||
andDo(MockMvcResultHandlers.print()) | ||
andExpect(status().isOk) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I left andDo
and andExpect
as methods that allow the user to transition to the new syntax. They can use the syntax they are familiar with as well as include their existing matchers.
By changing to print()
for printing the request and response then it forces the user to qualify MockMvcResultHandlers.print()
, but I think that is fine.
I haven't switched andDo
to handle
yet. I think we need some better examples before changing that. handle(document())
for RestDocs for example feels weird.
Thanks I will take it from here. |
After more thoughts, I have rewritten the DSL entirely and ended-up with that more idiomatic DSL which is still pretty close to mockMvc {
GET("/person/{name}", "Lee") {
print()
request {
secure = true
headers {
accept = listOf(MediaType.APPLICATION_JSON)
}
principal = Principal { "foo" }
}
expect {
status { isOk }
content { contentType("application/json;charset=UTF-8") }
jsonPath("$.name") { value("Lee") }
content { json("""{"someBoolean": false}""", false) }
}
}
POST("/person") {
request {
content = """{ "name": "foo" }"""
headers {
accept = listOf(MediaType.APPLICATION_JSON)
contentType = MediaType.APPLICATION_JSON
}
}
expect {
status {
isCreated
}
}
}
} See this commit for more details. Any thoughts @checketts @jnizet @gzoritchak? |
Note it is possible to remove the |
It looks good! The only questions I have are around extensiblility.
|
Here are my thoughts on this example alone. I'll comment on the code after, as comments to the commit.
|
@checketts I have updated my branch in order to take in account your feedback:
|
@jnizet Please find my feedback:
I have also removed the Latest version: mockMvc {
get("/person/{name}", "Lee") {
print()
secure = true
accept = MediaType.APPLICATION_JSON
headers {
contentLanguage = Locale.FRANCE
}
principal = Principal { "foo" }
expect {
status { isOk }
content { contentType("application/json;charset=UTF-8") }
jsonPath("$.name") { value("Lee") }
content { json("""{"someBoolean": false}""", false) }
}
}
post("/person") {
content = """{ "name": "foo" }"""
headers {
accept = listOf(MediaType.APPLICATION_JSON)
contentType = MediaType.APPLICATION_JSON
}
expect {
status {
isCreated
}
}
}
} I like it :-) |
Yeah it looks great! The only question I have left is: 'How can a user add a custom matcher?' (extending the expect block) |
@checketts Thanks, but I think there we need to stay closer to Here is my latest proposal which seems to me more consistent. The number of optional parameter is important but with named parameter it seems doable and this is more close to mockMvc {
request(GET, "/person/Lee",
secure = true,
accept = MediaType.APPLICATION_JSON,
headers = { contentLanguage = Locale.FRANCE },
principal = Principal { "foo" }) {
status { isOk }
print()
content { contentType("application/json;charset=UTF-8") }
jsonPath("$.name") { value("Lee") }
content { json("""{"someBoolean": false}""", false) }
}
request(POST, "/person",
content = """{ "name": "foo" }""",
headers = {
accept = listOf(MediaType.APPLICATION_JSON)
contentType = MediaType.APPLICATION_JSON
}) {
status { isCreated }
}
} I have pushed the updated implementation on my branch. Any thoughts? |
Hmm, I'm not a big fan, especially since the varargs for url variables is missing here, and is quite important, IMHO. Why not just chain calls, just the way we do it with The DSL could look like the following, which is actually closer to your original proposal:
This is actually closer to the Java DSL, while still being very Kotlinesque, IMHO. It allows doing everything you want to create the request. Since the get()/post()/put(), etc. functions take some I also like how the two blocks act on the two main entities of a REST call: the request, and the response. What do you think? |
Yeah, I was hesitating between both approaches, I will update my branch accordingly. Not sure about |
Yes, I chose |
I'm not sure I understand the need to separate the request/response. Is it just to keep the 2 separate visually? I thought the reworking was to preserve the call/expect ordering that @jnizet had noted. If we stayed with the
|
Latest iteration closer to original mockMvc.get("/person/{name}", "Lee") {
secure = true
accept = MediaType.APPLICATION_JSON
headers { contentLanguage = Locale.FRANCE }
principal = Principal { "foo" }
} andExpect {
status { isOk }
content { contentType("application/json;charset=UTF-8") }
jsonPath("$.name") { value("Lee") }
content { json("""{"someBoolean": false}""", false) }
} andDo {
print()
}
mockMvc.post("/person") {
content = """{ "name": "foo" }"""
headers {
accept = listOf(MediaType.APPLICATION_JSON)
contentType = MediaType.APPLICATION_JSON
}
} andExpect {
status { isCreated }
} |
This is much better than the one with all the optional arguments. If I don't need to customize the request then a minimal one would be: mockMvc.get("/person/{name}", "Lee") andExpect {
status { isOk }
} |
I like this last one a lot (except for |
I had a look at the code, and it confirms that this last version is the right one to me: it looks very Kotlinesque and the code makes it clear that it's actually a thin wrapper around the Java DSL, making the transition between both intuitive and natural. 👍 I'm not sure I understand why the classes are open, though. |
Glad you like it @checketts @jnizet :-) Yes Classes are open to allow users to add extensions. |
A class doesn't need to be open to add extensions to it: you can add extensions to String, Int, whatever. It only needs to be open if you want to create a subclass. But the DSL ( |
What you describe is true only for Java classes, Kotlin classes need to be open to allow users to add extensions on them. |
Please add default values for the Otherwise the minimal usecase requires you to customize the response, even when it isn't needed: mockMvc.get("/person/{name}", "Lee") {} andExpect { //<- note the extra curlies before the andExpect
status { isOk }
} |
The latest is definitely the best. Some of the extensions I had tried before weren't possible, and are possible again. Here is the comparison I've been working against: https://github.com/sdeleuze/spring-framework/pull/1/files Another finding: the infix fun andHandle(handler:MvcResult.()-> Unit): ResultActionsWrapper {
actions.andDo { it.handler() }
return this
} |
@jnizet Here is an example of extending the expects DSL: https://github.com/sdeleuze/spring-framework/pull/1/files#diff-525cfe15474d3e20b0902649bd2e8695R31 which in this case required it to be And here is where it is used: https://github.com/sdeleuze/spring-framework/pull/1/files#diff-525cfe15474d3e20b0902649bd2e8695R53 @Test
fun `hello json`() {
val name = "Petr"
mockMvc.get("/hello/$name").andExpectCustom(::ClintMatchers) {
"$.surname" jsonPathIs name //JsonPath
"surname" jsonPath { value("Petr") }
model<HelloPostDto>("helloPostDto") {
assertEquals("Balat", surname)
}
}
} I'm open to feedback on if this approach is even useful. I was mainly exploring if it was open enough to support these goals. |
Here is a better approach: if we make fun ResultActionsWrapper.andDocument(identifier: String, configure: DocumentationScope.() -> Unit): ResultActionsWrapper {
actions.andDo(DocumentationScope(identifier).apply(configure).document())
return this
} |
@jnizet My mistake, I thought it was required. Thanks for your help, I will be off during my week of vacation. I will check extensibility with you, document and add more tests when I will be back. |
Meanwile, I've polished a few things in the restdocs DSL, which is now fully documented, tested with 100% coverage, and with a DSL for preprocessors too. You can check it out at https://github.com/Ninja-Squad/spring-rest-docs-kotlin. I'm pretty happy with the result. With the handle() method, adding an |
I have the possibility to avoid On one side, I like the fact we avoid an artificial wrapper that can limit extensibility and make the DSL available in every Any thoughts @jnizet @checketts? |
The confusion between |
Another strike I noticed is that you can't chain a non-infix after an infix. For example if I created an |
This commit introduces a `MockMvc` Kotlin DSL via a set of extensions like `MockMvc.get` or `MockMvc.request` that provide a Kotlin idiomatic and easily discoverable API while staying close to the original Java API design. Closes spring-projectsgh-1951
🎉 |
The infix form limits the extensibility of the API and prevents calling `andReturn()`. See spring-projectsgh-1951
This DSL make working with
MockMvc
much cleaner.Example:
Highlights:
andDo(print())
that avoids static importsThis code is based on the work of @petrbalat and myself in https://github.com/petrbalat/kd4smt