Spring MVC 3.2のJSONのテスト
以前、Spring MVC 3.2のSpring MVC Testを触ったでSpring MVC 3.2から導入されたSpring MVCのテストを紹介しましたが、今回はJSONのテストについて紹介します。
今回のアプリケーションもGitHubに上げています。いろいろなSpringのサンプルをまとめています。
https://github.com/kuwalab/SpringSample
Eclipse 4.3でMaven+WTPのプロジェクトとして作成していますので、その環境であればプロジェクトをimportすることですぐに使えます。
サンプル中のspring_mvc32_json_testフォルダをEclipseのプロジェクトとしてインポートしてください。
テスト用のアプリケーション
テスト用のアプリケーションとして、簡易書籍管理アプリケーションを作ります。今回はJSONのテストのため、RESTでアクセスしてCRUDをひと通り行います。
Mavenの設定
Springは依存ライブラリーが多い上、JSON対応でもライブラリーが必要になるためMavenで管理したほうが楽です。
pom.xmlの全体は以下になります。
<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/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.example.spring_mvc32_json_test</groupId> <artifactId>spring-mvcjsontest</artifactId> <packaging>war</packaging> <version>0.0.1-SNAPSHOT</version> <name>mvcjsontest Maven Webapp</name> <url>http://maven.apache.org</url> <build> <finalName>spring_mvc32_json_test</finalName> <pluginManagement> <plugins> <plugin> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>1.7</source> <target>1.7</target> <encoding>UTF-8</encoding> </configuration> </plugin> </plugins> </pluginManagement> </build> <dependencies> <!-- Spring Framework --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>${spring.version}</version> <scope>test</scope> </dependency> <!-- AspectJ --> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>${aspectJ.version}</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjrt</artifactId> <version>${aspectJ.version}</version> </dependency> <!-- JUnit --> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>${junit.version}</version> <scope>test</scope> </dependency> <!-- Servlet --> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.0.1</version> <scope>provided</scope> </dependency> <dependency> <groupId>javax.servlet.jsp</groupId> <artifactId>jsp-api</artifactId> <version>2.2</version> <scope>provided</scope> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> <dependency> <groupId>javax.validation</groupId> <artifactId>validation-api</artifactId> <version>1.0.0.GA</version> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-validator</artifactId> <version>4.3.0.Final</version> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-validator-annotation-processor</artifactId> <version>4.3.0.Final</version> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.0.11</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>jcl-over-slf4j</artifactId> <version>1.7.5</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-core</artifactId> <version>2.1.3</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.1.3</version> </dependency> <dependency> <groupId>com.jayway.jsonpath</groupId> <artifactId>json-path</artifactId> <version>0.8.1</version> <scope>test</scope> </dependency> <dependency> <groupId>org.hamcrest</groupId> <artifactId>hamcrest-all</artifactId> <version>1.3</version> <scope>test</scope> </dependency> </dependencies> <properties> <spring.version>3.2.6.RELEASE</spring.version> <aspectJ.version>1.7.1</aspectJ.version> <junit.version>4.11</junit.version> </properties> </project>
Spring自体やSpring MVC、test関連は前回も取り上げたとおりです。今回は、JSONの読み書きをするためにSpringではJacksonライブラリーを使用するのが簡単です。
Jacksonは以下の2つのライブラリーが必要です。
<dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-core</artifactId> <version>2.1.3</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.1.3</version> </dependency>
また、JSONのテストをする際に、[json-path:https://code.google.com/p/json-path/]というライブラリーを使います。
<dependency> <groupId>com.jayway.jsonpath</groupId> <artifactId>json-path</artifactId> <version>0.8.1</version> <scope>test</scope> </dependency>
最後に、今回はJUnit4に付属のMatcherでは足りなかったためhamcrestライブラリーも入れています。
<dependency> <groupId>org.hamcrest</groupId> <artifactId>hamcrest-all</artifactId> <version>1.3</version> <scope>test</scope> </dependency>
書籍管理アプリケーション
書籍情報のCRUDはRESTに従い以下のURLでアクセスします。
メソッド | URL | アクション |
---|---|---|
GET | /books | 全書籍情報の取得 |
POST | /books | 書籍の新規登録 |
PUT | /books/{書籍ID} | 書籍IDの書籍情報の更新 |
DELETE | /books/{書籍ID} | 書籍IDの書籍情報の削除 |
データの取得は上記のURLでアクセスしますが、管理画面として/indexを使用します。
アプリケーションのコード
web.xmlやSpringの設定に関してはGitHubのコードを見ていただくとして、ControllerとJSP、JavaScriptのコードだけ確認します。
まずControllerです。
package com.example.spring.mvcjson; import java.util.ArrayList; import java.util.List; import org.springframework.http.MediaType; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseBody; @Controller public class BookController { private static List<Book> bookList; static { bookList = new ArrayList<>(); bookList.add(new Book("1", "よくわかるSpring", 3000)); bookList.add(new Book("2", "よくわかるJava", 3200)); bookList.add(new Book("3", "よくわかるJUnit", 2800)); } @RequestMapping(value = "/", method = RequestMethod.GET) public String index() { return "book/index"; } @RequestMapping(value = "/books", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE + ";charset=utf-8") @ResponseBody public List<Book> books() { return bookList; } @RequestMapping(value = "/books", method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE + ";charset=utf-8") @ResponseBody public Book insert(@RequestBody Book book) { bookList.add(book); return book; } @RequestMapping(value = "/books/{bookId}", method = RequestMethod.PUT, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE + ";charset=utf-8") @ResponseBody public Book update(@PathVariable("bookId") String initBookId, @RequestBody Book book) { for (int i = 0; i < bookList.size(); i++) { Book currentBook = bookList.get(i); if (currentBook.getBookId().equals(initBookId)) { currentBook.setBookId(book.getBookId()); currentBook.setBookName(book.getBookName()); currentBook.setPrice(book.getPrice()); return currentBook; } } return new Book(); } @RequestMapping(value = "/books/{bookId}", method = RequestMethod.DELETE, produces = MediaType.APPLICATION_JSON_VALUE + ";charset=utf-8") @ResponseBody public Book delete(@PathVariable String bookId) { for (int i = 0; i < bookList.size(); i++) { Book book = bookList.get(i); if (book.getBookId().equals(bookId)) { bookList.remove(i); return book; } } return new Book(); } }
次に、JSPです。
<%@page contentType="text/html; charset=utf-8" %><%-- --%><!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>書籍情報</title> </head> <body> <table> <thead> <tr> <th><input type="text" size="3" placeholder="id" id="insBookId"></th> <th><input type="text" size="20" placeholder="書名" id="insBookName"></th> <th><input type="text" size="5" placeholder="価格" id="insPrice"></th> <th><input type="button" value="追加" id="insert"></th> </tr> </thead> <tbody> </tbody> </table> <script src="js/lib/jquery-2.0.3.min.js"></script> <script src="js/lib/underscore-1.5.1.min.js"></script> <script src="js/book.js"></script> </body> </html>
最後にJavaScriptです。
var JST = {}; JST['tr'] = _.template( '<tr>' + '<th>' + '<input type="hidden" name="initBookId" value="<%- bookId %>">' + '<input type="text" size="3" placeholder="id" name="bookId" value="<%- bookId %>">' + '</th>' + '<th><input type="text" size="20" placeholder="書名" name="bookName" value="<%- bookName %>"></th>' + '<th><input type="text" size="5" placeholder="価格" name="price" value="<%- price %>"></th>' + '<th><input type="button" value="更新" name="update"><input type="button" value="削除" name="delete"></th>' + '</tr>' ); var $tbody = $('tbody'); // 登録済みの一覧を読み直す var getBookList = function() { $.ajax({ method: 'get', url: 'books', dataType: 'json' }).done(function(data) { var i; $tbody.empty(); for (i = 0; i < data.length; i++) { $tbody.append(JST['tr'](data[i])); } }).fail(function(jqXHR, textStatus, errorThrown) { console.log(textStatus); }); }; var setTrEvent = function() { // 行ごとの更新ボタンの処理 $('table').on('click', 'input[name="update"]', function() { var $tr = $(this).parents('tr'); var sendData = { bookId: $tr.find('input[name="bookId"]').val(), bookName: $tr.find('input[name="bookName"]').val(), price: $tr.find('input[name="price"]').val() }; var initBookId = $tr.find('input[name="initBookId"]').val(); $.ajax({ method: 'put', contentType: 'application/json;charset=utf-8', data: JSON.stringify(sendData), url: 'books/' + initBookId, dataType: 'json' }).done(function(data) { getBookList(); }).fail(function(jqXHR, textStatus, errorThrown) { console.log(textStatus); }); }); // 行ごとのdeleteボタンの処理 $('table').on('click', 'input[name="delete"]', function() { var bookId = $(this).parents('tr').find('input[name="initBookId"]').val(); $.ajax({ method: 'delete', url: 'books/' + bookId, dataType: 'json' }).done(function(data) { getBookList(); }).fail(function(jqXHR, textStatus, errorThrown) { console.log(textStatus); }); }); }; getBookList(); setTrEvent(); $('#insert').on('click', function() { var sendData = { bookId: $('#insBookId').val(), bookName: $('#insBookName').val(), price: $('#insPrice').val() }; $.ajax({ method: 'post', contentType: 'application/json;charset=utf-8', data: JSON.stringify(sendData), url: 'books', dataType: 'json' }).done(function(data) { $('#insBookId').val(''); $('#insBookName').val(''); $('#insPrice').val(''); getBookList(); }).fail(function(jqXHR, textStatus, errorThrown) { console.log(textStatus); }); });
データの読み込み
/コンテキストパス/にアクセスすると、データを読み込みます。データはJavaScriptのgetBookList関数で読み込んでいます。
$.ajax({ method: 'get', url: 'books', dataType: 'json' }).done(function(data) { var i; $tbody.empty(); for (i = 0; i < data.length; i++) { $tbody.append(JST['tr'](data[i])); } //
関数では、/booksにgetリクエストを投げ、戻り値をJSONとして受け取ります。
受け取ったあとは、Underscore.jsのテンプレート機能でレンダリングしています。
データの登録
データの登録は追加ボタンを押すことで、postリクエストを投げます。データの読み込みと違い、登録するデータをJSONで送信します。
$('#insert').on('click', function() { var sendData = { bookId: $('#insBookId').val(), bookName: $('#insBookName').val(), price: $('#insPrice').val() }; $.ajax({ method: 'post', contentType: 'application/json;charset=utf-8', data: JSON.stringify(sendData), url: 'books', dataType: 'json' }).done(function(data) { $('#insBookId').val(''); $('#insBookName').val(''); $('#insPrice').val(''); getBookList(); }).fail(function(jqXHR, textStatus, errorThrown) { console.log(textStatus); }); });
データの登録に成功した場合には、入力フォームの情報を削除しデータを再度読み込みます。
データの更新
データの更新は、更新ボタンを押すことでputリクエストを投げます。更新はURLのパラメータに更新するIDを埋め込み、更新するデータはJSONデータとして送信します。
書籍ID自体も変更することができるため、データを読み込んだ辞典での書籍IDをhiddenパラメータとして保管しておきURLに埋め込む書籍IDはそのデータを使用します。画面上に表示される書籍IDは更新する値となります。
$('table').on('click', 'input[name="update"]', function() { var $tr = $(this).parents('tr'); var sendData = { bookId: $tr.find('input[name="bookId"]').val(), bookName: $tr.find('input[name="bookName"]').val(), price: $tr.find('input[name="price"]').val() }; var initBookId = $tr.find('input[name="initBookId"]').val(); $.ajax({ method: 'put', contentType: 'application/json;charset=utf-8', data: JSON.stringify(sendData), url: 'books/' + initBookId, dataType: 'json' }).done(function(data) { getBookList(); }).fail(function(jqXHR, textStatus, errorThrown) { console.log(textStatus); }); });
データの削除
データの削除はupdateと同じようにhidden項目の書籍IDを用いてデータを削除します。
JSONのテスト
テストコードの全体。
package com.example.spring.mvcjson; import static org.hamcrest.Matchers.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; import static org.springframework.test.web.servlet.setup.MockMvcBuilders.*; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; import org.springframework.web.context.WebApplicationContext; import com.fasterxml.jackson.databind.ObjectMapper; @RunWith(SpringJUnit4ClassRunner.class) @WebAppConfiguration @ContextConfiguration(locations = { "file:src/main/webapp/WEB-INF/spring/beans-webmvc.xml" }) public class BookControllerTest { @Autowired private WebApplicationContext wac; private MockMvc mockMvc; @Before public void setup() { mockMvc = webAppContextSetup(wac).build(); } /* JSONPath http://goessner.net/articles/JsonPath/ */ @Test public void slash_booksのGET() throws Exception { mockMvc.perform(get("/books")).andExpect(jsonPath("$").isArray()) .andExpect(jsonPath("$", hasSize(3))) .andExpect(jsonPath("$[0].bookId").value("1")) .andExpect(jsonPath("$[0].bookName").value("よくわかるSpring")) .andExpect(jsonPath("$[0].price").value(3000)); } @Test public void slash_booksのPOST() throws Exception { Book book = new Book("123", "よくわかるJSON", 2999); ObjectMapper mapper = new ObjectMapper(); String jsonStr = mapper.writerWithDefaultPrettyPrinter() .writeValueAsString(book); mockMvc.perform( post("/books").contentType(MediaType.APPLICATION_JSON).content( jsonStr.getBytes())) .andExpect(jsonPath("$.bookId").value(book.getBookId())) .andExpect(jsonPath("$.bookName").value(book.getBookName())) .andExpect(jsonPath("$.price").value(book.getPrice())); // 追加されているか確認 mockMvc.perform(get("/books")).andExpect(jsonPath("$").isArray()) .andExpect(jsonPath("$", hasSize(4))) .andExpect(jsonPath("$[3].bookId").value(book.getBookId())) .andExpect(jsonPath("$[3].bookName").value(book.getBookName())) .andExpect(jsonPath("$[3].price").value(book.getPrice())); // 追加したものを削除しておく mockMvc.perform(delete("/books/" + book.getBookId(), "json")); // 削除されているか確認 mockMvc.perform(get("/books")).andExpect(jsonPath("$").isArray()) .andExpect(jsonPath("$", hasSize(3))); } @Test public void slash_booksのPUT() throws Exception { Book book = new Book("5", "よくわかるJava 7", 4000); ObjectMapper mapper = new ObjectMapper(); String jsonStr = mapper.writerWithDefaultPrettyPrinter() .writeValueAsString(book); mockMvc.perform( put("/books/2").contentType(MediaType.APPLICATION_JSON) .content(jsonStr.getBytes())) .andExpect(jsonPath("$.bookId").value(book.getBookId())) .andExpect(jsonPath("$.bookName").value(book.getBookName())) .andExpect(jsonPath("$.price").value(book.getPrice())); // 変更されているか確認 mockMvc.perform(get("/books")).andExpect(jsonPath("$").isArray()) .andExpect(jsonPath("$", hasSize(3))) .andExpect(jsonPath("$[1].bookId").value(book.getBookId())) .andExpect(jsonPath("$[1].bookName").value(book.getBookName())) .andExpect(jsonPath("$[1].price").value(book.getPrice())); // 追加したものをもとに戻しておく book = new Book("2", "よくわかるJava", 3200); jsonStr = mapper.writerWithDefaultPrettyPrinter().writeValueAsString( book); mockMvc.perform( put("/books/5").contentType(MediaType.APPLICATION_JSON) .content(jsonStr.getBytes())) .andExpect(jsonPath("$.bookId").value(book.getBookId())) .andExpect(jsonPath("$.bookName").value(book.getBookName())) .andExpect(jsonPath("$.price").value(book.getPrice())); } }
読み込みデータのテスト
データを取得は、/booksに対してgetリクエストを投げます。レスポンスはJSONで帰ってきますが、そのテストはjson-pathを使います。
@Test public void slash_booksのGET() throws Exception { mockMvc.perform(get("/books")) .andExpect(jsonPath("$").isArray()) .andExpect(jsonPath("$", hasSize(3))) .andExpect(jsonPath("$[0].bookId").value("1")) .andExpect(jsonPath("$[0].bookName").value("よくわかるSpring")) .andExpect(jsonPath("$[0].price").value(3000)); }
レスポンスは、以下のように書籍情報の配列として取得できます。
[{"bookId":"1","bookName":"よくわかるSpring","price":3000}, {"bookId":"2","bookName":"よくわかるJava","price":3200}, {"bookId":"3","bookName":"よくわかるJUnit","price":2800}]
json-pathは以下のドキュメントを参考にしてください。
https://code.google.com/p/json-path/
簡単に解説すると、$がルート要素を表します。今回は配列のため$[0]で最初の書籍、$[1]で2番目の書籍を表します。また、オブジェクトのアクセスは「.」を用いて行えます。最初の書籍の書籍IDは$[0].bookIdのようにアクセスできます。
json-pathを使うためには、MockMvcResultMatchers#jsonPathを使用します。
jsonPathの引数には上記のjson-pathの式を入れます。戻り値はJsonPathResultMathersとなります。
JsonPathResultMathersの主なメソッドは以下となります。
メソッド | 説明 |
---|---|
ResultMatcher doesNotExist() | 指定のpathが存在しない |
ResultMatcher exists() | 指定のpathが存在する |
ResultMatcher isArray() | 指定のpathが配列 |
取得した結果をmatcherで検査 | |
ResultMatcher value(Object expectedValue) | 取得した結果をexpectedValueで検査 |
データの変更のテスト
データの追加、更新、削除はまとめて行っています。staticフィールドに対して追加するので、追加後に削除してもとに戻しています。
@Test public void slash_booksのPOST() throws Exception { Book book = new Book("123", "よくわかるJSON", 2999); ObjectMapper mapper = new ObjectMapper(); String jsonStr = mapper.writerWithDefaultPrettyPrinter() .writeValueAsString(book); mockMvc.perform( post("/books").contentType(MediaType.APPLICATION_JSON).content( jsonStr.getBytes())) .andExpect(jsonPath("$.bookId").value(book.getBookId())) .andExpect(jsonPath("$.bookName").value(book.getBookName())) .andExpect(jsonPath("$.price").value(book.getPrice())); // 追加されているか確認 mockMvc.perform(get("/books")).andExpect(jsonPath("$").isArray()) .andExpect(jsonPath("$", hasSize(4))) .andExpect(jsonPath("$[3].bookId").value(book.getBookId())) .andExpect(jsonPath("$[3].bookName").value(book.getBookName())) .andExpect(jsonPath("$[3].price").value(book.getPrice())); // 追加したものを削除しておく mockMvc.perform(delete("/books/" + book.getBookId(), "json")); // 削除されているか確認 mockMvc.perform(get("/books")).andExpect(jsonPath("$").isArray()) .andExpect(jsonPath("$", hasSize(3))); } @Test public void slash_booksのPUT() throws Exception { Book book = new Book("5", "よくわかるJava 7", 4000); ObjectMapper mapper = new ObjectMapper(); String jsonStr = mapper.writerWithDefaultPrettyPrinter() .writeValueAsString(book); mockMvc.perform( put("/books/2").contentType(MediaType.APPLICATION_JSON) .content(jsonStr.getBytes())) .andExpect(jsonPath("$.bookId").value(book.getBookId())) .andExpect(jsonPath("$.bookName").value(book.getBookName())) .andExpect(jsonPath("$.price").value(book.getPrice())); // 変更されているか確認 mockMvc.perform(get("/books")).andExpect(jsonPath("$").isArray()) .andExpect(jsonPath("$", hasSize(3))) .andExpect(jsonPath("$[1].bookId").value(book.getBookId())) .andExpect(jsonPath("$[1].bookName").value(book.getBookName())) .andExpect(jsonPath("$[1].price").value(book.getPrice())); // 追加したものをもとに戻しておく book = new Book("2", "よくわかるJava", 3200); jsonStr = mapper.writerWithDefaultPrettyPrinter().writeValueAsString( book); mockMvc.perform( put("/books/5").contentType(MediaType.APPLICATION_JSON) .content(jsonStr.getBytes())) .andExpect(jsonPath("$.bookId").value(book.getBookId())) .andExpect(jsonPath("$.bookName").value(book.getBookName())) .andExpect(jsonPath("$.price").value(book.getPrice())); }