目的
先輩「MockMvcでJSONPathを使おうと思ってて…」
@takkii1010 「…?」
となったので、以下を簡単にまとめたい。
- JSONPathとは何者か
- JSONPathの構文
- MockMvcでの使い方
1. JSONPathとは何者か
Data may be interactively found and extracted out of JSON structures on the client without special scripting.
JSONPath expressions always refer to a JSON structure in the same way as XPath expression are used in combination with an XML document.
- XPath for JSON
- 特別なスクリプトを書かずとも、JSONを双方向に検出&抽出できるライブラリ
- XPathがXMLを参照するのと同様に、JSONPathは常にJSONを参照するようになっている
- 簡単にいうと、JSONから欲しいkey,value,lengthなどを取得できて便利
- 意外と身近なところで使われている
- Oracle Database, Kubectl etc.
2. JSONPathの構文
Jayway JsonPath を見ながら進めます。
また、以下サイトで、JSONPathのお試しがブラウザから簡単にできます。
基本構文
使ったJSONはこちら。
{
"firstName": "John",
"lastName" : "doe",
"age" : 26,
"address" : {
"streetAddress": "naist street",
"city" : "Nara",
"postalCode" : "630-0192"
},
"phoneNumbers": [
{
"type" : "iPhone",
"number": "0123-4567-8888"
},
{
"type" : "home",
"number": "0123-4567-8910"
}
]
}
JSONPath | 説明 | 例 | 結果 | メモ |
---|---|---|---|---|
$ | ルートの要素 | $.firstName |
"John" |
$ だけだと全部取得 |
@ | フィルター結果の現在の要素 | $.phoneNumbers[?(@.type == 'home')] |
"type": "home", "number": "0123-4567-8910"
|
|
* | ワイルドカード | $.address.* |
"naist street", "Nara", "630-0192"
|
|
.. | keyが一致するすべてのvalue | $..type |
"iPhone", "home"
|
|
['<name>' (, '<name>')] | ブラケット演算子 | $.['firstName'] |
"John" |
|
[<number> (, <number>)] | $.phoneNumbers[0,1] |
{ "type": "iPhone", "number": "0123-4567-8888" }, { "type": "home", "number": "0123-4567-8910" }
|
||
[start:end] | 配列のスライス | $.phoneNumbers[0:1] |
{ "type": "iPhone", "number": "0123-4567-8888" }
|
|
[?(<expression>)] | フィルター機能。式はBooleanで評価されないといけない |
@ に例記載。 |
…結果がきれいに書けてない。
関数
min()
, max()
, avg()
, stddev()
, length()
, sum()
があります。
length()
は $.phoneNumbers.length
とあまり深く考えずに使えますが、他の関数が少し厄介。
こんな感じのJSONの場合…
{
"id": 1,
"name" : "スクエ…",
"products": [
{
"name" : "FINAL FANTASY VII",
"price": 1572
},
{
"name" : "FINAL FANTASY X",
"price": 8800
},
{
"name" : "FINAL FANTASY XV",
"price": 8800
}
]
}
一度取り出してから、処理を行う必要があるらしい。
https://oboe2uran.hatenablog.com/entry/2018/05/06/131634
MockMvcを使う場合は以下で動きました。
val jsonObject = mockMvc.perform(get("/gameCompanies/1"))
.andExpect(status().isOk())
.andReturn().response.contentAsString
val priceList: List<Int> = JsonPath.read( jsonObject, "\$.products[*].price")
val min: Int = JsonPath.read(priceList, "\$.min()")
val max: Int = JsonPath.read(priceList, "\$.max()")
val sum: Int = JsonPath.read(priceList, "\$.sum()")
val stddev: Int = JsonPath.read(priceList, "\$.stddev()")
assertThat(min).isEqualTo(1572)
assertThat(max).isEqualTo(8800)
assertThat(sum).isEqualTo(19172)
assertThat(stddev).isEqualTo(3407)
3. MockMvcでの使い方
@RunWith(SpringRunner::class)
@WebMvcTest(GameCompanyController::class)
class GameCompanyControllerTest {
@Autowired
lateinit var mockMvc: MockMvc
@MockBean
lateinit var gameCompanyRepository: GameCompanyRepositoryInterface
@Test
fun getGameCompanyTest() {
val expectedGameCompany = GameCompany(
id = 1L,
name = "スクエ…",
products = listOf(
Product("FINAL FANTASY VII", BigDecimal(1572)),
Product("FINAL FANTASY X", BigDecimal(8800)),
Product("FINAL FANTASY XV", BigDecimal(8800))
)
)
`when`(gameCompanyRepository.fetchGameCompany(any())).thenReturn(expectedGameCompany)
mockMvc.perform(get("/gameCompanies/1")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("\$.id").value(1))
.andExpect(jsonPath("\$.name").value("スクエ…"))
.andExpect(jsonPath("\$.products[0].name").value("FINAL FANTASY VII"))
.andExpect(jsonPath("\$.products[0].price").value(1572))
.andExpect(jsonPath("\$.products[1].name").value("FINAL FANTASY X"))
.andExpect(jsonPath("\$.products[1].price").value(8800))
.andExpect(jsonPath("\$.products[2].name").value("FINAL FANTASY XV"))
.andExpect(jsonPath("\$.products[2].price").value(8800))
}
}
Kotlinの indices
使うとこんな感じ。
…
mockMvc.perform(get("/gameCompanies/1")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
...
.andExpect(expectedGameCompany.products)
}
private fun ResultActions.andExpect(products: List<Product>): ResultActions {
for (index in products.indices) {
andExpect(jsonPath("\$.products[$index].name").value(products[index].name))
.andExpect(jsonPath("\$.products[$index].price").value(products[index].price))
}
return this
}
他にも、 Matchers
のisを使用しても動きます。
mockMvc.perform(get("/gameCompanies/1")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("\$.id", `is`(1)))
ただ、少し古い書き方のようなので、 value
で書くのが良さそう。
参考にしたサイト一覧
https://goessner.net/articles/JsonPath/
https://www.techscore.com/tech/XML/XPath/XPath1/xpath01.html/
https://github.com/json-path/JsonPath
http://oboe2uran.hatenablog.com/entry/2018/05/06/131634