Node EasyMock で API モックサーバを作る
Node EasyMock を使って、API モックを作ってみた。
フロントエンド:Angular JS
バックエンド:Spring Boot
という感じでアプリを作っているのですが、フロントエンドから作りたくなったので使ってみました。
インストール
基本的には README の通り
$ npm install -g easymock $ easymock
で動作しました。
ただし、node のバージョンの指定があるようで v5.0.0 で実行したときに下記のメッセージが表示されました。
wanted: {"node":">= 0.10.0 < 0.11.0 || >= 0.11.13 < 0.13.0 || >= 1.0.0 < 2.0.0"} (current: {"node":"5.0.0","npm":"3.3.6"})
ということで、今は v0.12.7 を使用しています。
API モック
Node EasyMock は URL、HTTP Method と対応する JSON ファイルの内容を返却します。
例えば、GET /users
をリクエストした場合 users_get.json ファイルの内容を返却します。
[URL]_[HTTP Method].json
の形式でファイルを用意するだけで、API モックを作ることができます。
また、/users/1/
などのネストした URL の場合は users ディレクトリ配下に 1_get.json
を配置します。
ログとドキュメント
Node EasyMock はログとドキュメントを表示する機能もあります。
/_documentation/
にアクセスすると API ドキュメントを表示することができます。
ドキュメントでは API の一覧を確認することができます。
エンドポイントをクリックすると、対象のエンドポイントのレスポンスを確認することができます。
同様に /_logs/
にアクセスすると、アクセスログを確認することができます。
まとめ
フロントエンドの開発とバックエンドの開発が並行して進む場合など、バックエンドがまだ出来ていないときに利用してみてはいかがでしょうか? JSON ファイルのみ用意できれば使用できるので、学習コストも低くて良いと思います。
GitHub を使ったプロジェクト管理ツール Waffle を使ってみた
先日面白い記事を見つけました。
GitHub の issue、Pull Request を使ってプロジェクト管理を行うためのツールです。
この中でも、Waffle が使いやすいというコメントをいただいたので、早速使ってみました。
使い方
画面はこんな感じです。
Backlog, Ready, In Progress, Done にカラムが分かれていて、 ドラッグ&ドロップで issue を移動させることができます。
また Waffle はコミットコメント、Pull Request のコメントを使って issue を移動させることができます。
close #xx、fix #xx のようにキーワードと issue 番号を記述した Pull Request がマージされると、対象の issue をクローズ してくれます。
サンプルとして「ワッフル テスト issue」という issue を作成しました。
この issue を自動的にクローズさせます。
自動的に移動させるには前述のとおり キーワードと issue 番号 をコミットコメントに含める必要があります。
キーワードは下記のとおりです。
- close, closes, closed
- fix, fixes, fixed
- resolve, resolves, resolved
「ワッフル テスト issue」は #17 のため
$ git commit -m "fix #17"
とコミットコメントに記載することで自動的に issue をクローズします。
コミットしたブランチを Push し、Pull Request を作成します。
すると、issue と Pull Request が紐づけられ、自動的に issue が In Progress に移動します。
最後に Pull Request をマージすると issue, Pull Request が Done に移動します。
これで issue を自動的にクローズさせることができました。
issue と Pull Request の紐付き
この記事を書くために Waffle を触っていたのですが、
前述のとおり issue と Pull Request が 1つに合わさったような見た目になる場合と、ならない場合がありました。
結果として Pull Request のタイトルもしくはコメントにキーワードと issue 番号を含めた場合 1つに合わさったような見た目になることがわかりました。
コミットコメントに issue 番号を入れれば、クローズすることはできるので、
最低限コミットコメントにキーワードと issue 番号を含め、
余力があれば Pull Request のコメントに書くことで
どの Pull Request でどの issue が対応されたのか さらに分かりやすくなるのではと思いました。
まとめ
コミットコメントにキーワードと issue 番号を書くだけで、ステータス管理をしてくれるのはとても楽だと感じました。
SCM、チケット管理 etc... 様々なツールを使うとあちこち見なければいけないので、1つにまとまってるのは良いですね。
Lombok でコード量を削減!
前回の記事でも書きましたが、Spring Boot のサンプルではあえて Lombok を使用しませんでした。
理由はまだ Lombok の解説をしていなかったので。
ということで、Lombok の解説をしようと思います。
What's Lombok
簡単にいうと、Java のお決まりのコードを排除するためのライブラリです。 Getter/Setter とかコンストラクタとかお決まりで書いているコードをアノテーションを付けることで排除できるようになります。(すごい!)
導入
Lombok は依存関係に追加する他に、IDE にも設定する必要があります。
- 依存関係の解決 公式にも記載されていますが、 Gradle を使う場合は下記の通り
provided "org.projectlombok:lombok:1.16.6"
ちなみに provided
は apply plugin: 'war'
を指定しないと使えないので、指定していない場合は compile
に変更してください。
サンプル
サンプルに Spring Boot の時に使用したエンティティをリファクタします。
元ソース
package hello.domain; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.Table; @Entity @Table(name = "tasks") public class Task { @Id @GeneratedValue private Integer id; @Column(nullable = false) private String context; public Task() { } public Task(Integer id, String context) { this.id = id; this.context = context; } public Integer getId() { return id; } public String getContext() { return context; } }
まずはコンストラクタを削除
今回、引数なしのコンストラクタと、すべてのフィールドを持つコンストラクタが定義されているので、この 2 つを Lombok で生成します。
引数なしのコンストラクタは @NoArgsConstructor
、すべてのフィールドを持つコンストラクタは @AllArgsConstructor
で生成できます。
package hello.domain; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.Table; import lombok.AllArgsConstructor; import lombok.NoArgsConstructor; @Entity @Table(name = "tasks") @NoArgsConstructor @AllArgsConstructor public class Task { @Id @GeneratedValue private Integer id; @Column(nullable = false) private String context; public Integer getId() { return id; } public String getContext() { return context; } }
同様に Getter/Setter も削除します。
@Getter
、@Setter
アノテーションもあるのですが、データを管理するクラスのためのアノテーション @Data
を付与します。
@Data
は @Getter
、@Setter
アノテーションを含んでいるので、付与されます。
package hello.domain; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.Table; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @Entity @Table(name = "tasks") @NoArgsConstructor @AllArgsConstructor public class Task { @Id @GeneratedValue private Integer id; @Column(nullable = false) private String context; }
コード量がかなり少なくなりました。
Outline を見ると、ちゃんとコンストラクタ、Getter/Setter が定義されていますね。
その他のアノテーション
Lombok には他にも様々なアノテーションがあります。
@Data
, @Value
, @Builder
辺りはすぐに導入できると思います。
@Data
上でも解説しましたが、POJO や Beans に使用するアノテーションです。
また、POJO や Beans に使用するアノテーション @ToString
, @EqualsAndHashCode
, @Getter
, @Setter
, @RequiredArgsConstructor
も自動で付与されます。
@ToString
デバッグがしやすいように、クラスのフィールド名、フィールドの値を出力するように toString メソッドをオーバーライドします。
@EqualsAndHashCode
オブジェクトに定義されているフィールドの値で equails, hashCode を算出できるように equails, hashCode メソッドをオーバーライドします。
プレーンな Java だと Bean クラスのフィールドの値が一緒でも、equals は false になってしまうので、開発者がいちいち再定義するのは面倒なのでありがたいです。
@Value
イミュータブル(不変の値)を持つクラスを簡単に生成するアノテーションです。
イミュータブルなので値の変更ができないため、フィールドはデフォルトで private, final になります。setter は生成されません。
@Builder
ビルダーパターンを簡単に生成するアノテーションです。
以上です。
Lombok はデファクトスタンダードになると思います。すでにデファクトスタンダードかもしれません。
メリットの方が大きいと思うので、積極的に導入しはいかがでしょうか。
Java Spring Boot その8
7回に渡って Spring Boot について記事を書いてきたので、Spring Boot を触ってみた感想を書いてみようと思います。
良かった点
設定ファイル(XML) が少ない
今までのサンプルを通して、設定ファイルは 1
回も書きませんでした。
Spring というと XML というイメージがあったのですが、全く書かなくてここまで出来るとは思っていませんでした。
実装が揃っている
リポジトリクラスを作成した時に驚いたのですが、よく使う実装を用意してくれているのがすごく有り難いですね。
DRY に繋がると思います。
starter が良い
使いたい機能の starter を設定すれば、必要な依存が自動的に追加される。依存関係の解決がとても楽でした。
Java というとライブラリ依存が激しいので、この機能は開発者にとって楽だと思います。
アノテーションが理解しやすい
こういう意味のアノテーションなんだろうな。というのが名前から推測しやすかったです。
記述量が少ない
実装が揃っている、starterが良い と繋がると思うのですが、
これらのお陰で記述量がかなり削減できていると思います。
組み込み Tomcat が良い
Java の Web アプリケーションというと Tomcat を用意して、Eclipse に Tomcat プラグイン入れて…と色々面倒ですが、
Tomcat が組み込まれていることで、プレーンの Java を実行すればよいのはとても楽でした。
リリース時等も楽なのだろうなと思いながら、実装していました。
Spring Loaded が良い
Spring Boot の利点とは少し違いますが、Spring Loaded が結構効いてくれて、アプリケーションサーバの再起動回数が大幅に削減できました。 再起動自体も組み込み Tomcat のお陰で楽ですし。
気になった点
記述量が多い
上記の記述量が少ないと全く反対のことを書いていますが、Spring Boot として記述量が多いのではなく、Java として。
やっぱり Ruby と比べてると記述量が多いなって感じます。
今回あえて lombok を使わなかったのですが、lombok 使わないとこんなに書かなきゃいけないのかと思いました。
静的型付け言語なので、安心感は大きく得られるのですけどね。
以上です。
次回からは Spring Boot から少し離れようと思います。
Java Spring Boot その7
今回は DB 実装部分のソースを紹介します。
エンティティ
package hello.domain; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.Table; @Entity @Table(name = "tasks") public class Task { @Id @GeneratedValue private Integer id; @Column(nullable = false) private String context; public Task() { } public Task(Integer id, String context) { this.id = id; this.context = context; } public Integer getId() { return id; } public String getContext() { return context; } }
@Entity
エンティティクラスを表すためのアノテーション。
@Table(name = "tasks")
エンティティに紐づくテーブルを指定するアノテーション。デフォルトではエンティティ名がクラス名となります。
@Id
主キーのフィールドを表すアノテーション。
@GeneratedValue
主キーが自動採番されることを表す。生成方法を指定することもできる。
@Column(nullable = false)
カラムに対する設定を行うアノテーション。
nullable = false
と設定し、Not Null であることを設定。
リポジトリ
package hello.repository; import org.springframework.data.jpa.repository.JpaRepository; import hello.domain.Task; public interface TaskRepository extends JpaRepository<Task, Integer> { }
JpaRepository を継承したインターフェースを作成するだけで、取得、登録、更新、削除ができるようになります。(すごい!)
JpaRepository<Task, Integer>
のようにジェネリクスに対象のエンティティ、IDカラムのデータ型を宣言し、終了です。
サービス
package hello.service; import java.util.List; import javax.transaction.Transactional; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import hello.domain.Task; import hello.repository.TaskRepository; @Service @Transactional public class TaskService { @Autowired TaskRepository taskRepository; public List<Task> findAll() { return taskRepository.findAll(); } public Task findOne(Integer id) { return taskRepository.findOne(id); } public Task create(Task task) { return taskRepository.save(task); } public Task update(Task task) { return taskRepository.save(task); } public void delete(Integer id) { taskRepository.delete(id); } }
@Service
サービスクラスを表すアノテーション
@Transactional
トランザクション境界を設定するアノテーション。
サービスに設定したため、トランザクション境界はサービスとなります。
@Autowired
DI (依存性注入) を行うためのアノテーション。
Autowired アノテーションを記述すると、自動的に DI を解決してくれます。
サービスクラス自体はリポジトリクラスの内容を呼び出す形なので特に説明はいらないと思います。
コントローラ
package hello; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.context.annotation.ComponentScan; import org.springframework.http.HttpStatus; 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.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import hello.domain.Task; import hello.service.TaskService; @RestController @RequestMapping("api/tasks") @EnableAutoConfiguration @ComponentScan public class RestSampleController { @Autowired TaskService taskService; @RequestMapping(method = RequestMethod.GET) List<Task> getTasks() { // GET api/tasks で実行されるメソッド return taskService.findAll(); } @RequestMapping(value = "{id}", method = RequestMethod.GET) Task getTask(@PathVariable Integer id) { // GET api/tasks/{id} で実行されるメソッド return taskService.findOne(id); } @RequestMapping(method = RequestMethod.POST) @ResponseStatus(HttpStatus.CREATED) Task createTask(@RequestBody Task task) { // POST api/tasks で実行されるメソッド return taskService.create(task); } @RequestMapping(value = "{id}", method = RequestMethod.PUT) Task updateTask(@PathVariable Integer id, @RequestBody Task task) { // PUT api/tasks/{id} で実行されるメソッド return taskService.update(task); } @RequestMapping(value = "{id}", method = RequestMethod.DELETE) @ResponseStatus(HttpStatus.NO_CONTENT) void deleteTask(@PathVariable Integer id) { // DELETE api/tasks で実行されるメソッド taskService.delete(id); } public static void main(String[] args) { SpringApplication.run(RestSampleController.class, args); } }
大きな変更はなく、モックデータを削除して、サービスクラスからデータを取得するようにした位です。
@ComponentScan
必要なコンポーネントを自動で読み込んでくれるアノテーションです。
これによりサービス、リポジトリ、エンティティクラスが読み込まれます。
以上で DB 実装部分は終了です。
Java Spring Boot その6
前回のモック部分を DB 接続に変更します。
その前に、今回使う DB、O/R マッパーや概念の説明を先にします。
DB, O/Rマッパーについて
DB は H2、O/R マッパーは Spring Data JPA を使用します。
O/R マッパーというのは DB のデータをオブジェクトにマッピングするためのライブラリです。
DB のデータを Java のクラス、オブジェクトとして扱うためのライブラリだと思ってください。
JPA は Java Persistence API の略で、Java の O/R マッパーの仕様です。
Spring Data JPA は JPA を拡張したライブラリです。
クラス構成
また、今回は下記の通りクラスを分割し、責務を分散します。
- エンティティ
- リポジトリ
- サービス
- コントローラ
エンティティ
DB の情報を Java オブジェクトに表したもの テーブルがクラス、カラムがフィールドになっている。
リポジトリ
エンティティを登録、検索などといった処理を担うクラス。DB のデータを取得し、エンティティへマッピングする処理等を行う。
DB からのデータ取得方法などはリポジトリクラスに隠蔽させる。
サービス
ビジネスロジックを記述する。リポジトリを使用してデータの取得等もこのクラスが行う。
コントローラ
リクエストをマッピングするクラス
サービスクラスを利用してリクエストに必要な処理を実行する。
MVC のレイヤーと、処理内容を表にすると
リクエスト | → | 処理 | → | DB | |
---|---|---|---|---|---|
アプリケーション層 | コントローラ | ||||
ドメイン層 | サービス | ||||
ドメイン層 | リポジトリ | ||||
ドメイン層 | エンティティ |
だと私は考えています。※あくまで私の考えです。
今回はここまで。実際のソースは次の記事で。
Java Spring Boot その5
前回の続きで POST, PUT, DELETE メソッドを作成をします。
ソース
POST, PUT, DELETE を追加したソースは下記の通りです。
また、モックデータの作成方法を変更しました。
package hello; import java.util.ArrayList; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.IntStream; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.http.HttpStatus; 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.ResponseStatus; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("api/tasks") @EnableAutoConfiguration public class RestSampleController { static class Task { public Task() { } public Task(Integer id, String context) { this.id = id; this.context = context; } public Integer id; public String context; } static Map<Integer, Task> tasks = new HashMap<>(); @RequestMapping(method = RequestMethod.GET) List<Task> getTasks() { // GET api/tasks で実行されるメソッド return new ArrayList<>(tasks.values()); } @RequestMapping(value = "{id}", method = RequestMethod.GET) Task getTask(@PathVariable Integer id) { // GET api/tasks/{id} で実行されるメソッド return tasks.get(id); } @RequestMapping(method = RequestMethod.POST) @ResponseStatus(HttpStatus.CREATED) Task createTask(@RequestBody Task task) { // POST api/tasks で実行されるメソッド int nextIndex = tasks.keySet().stream().max(Comparator.naturalOrder()).get() + 1; task.id = nextIndex; tasks.put(nextIndex, task); return task; } @RequestMapping(value = "{id}", method = RequestMethod.PUT) Task updateTask(@PathVariable Integer id, @RequestBody Task task) { // PUT api/tasks/{id} で実行されるメソッド Task targetTask = tasks.get(id); if (targetTask == null) { return null; } targetTask.context = task.context; tasks.put(id, targetTask); return targetTask; } @RequestMapping(value = "{id}", method = RequestMethod.DELETE) @ResponseStatus(HttpStatus.NO_CONTENT) void deleteTask(@PathVariable Integer id) { // DELETE api/tasks で実行されるメソッド tasks.remove(id); } public static void main(String[] args) { // モックデータ作成 IntStream.range(0, 3).forEach(index -> tasks.put(index, new Task(index, String.format("sample%d", index)))); SpringApplication.run(RestSampleController.class, args); } }
解説
前回解説した通り、@RequestMapping
で POST, PUT, DELETE 各メソッドにマッピングする Java のメソッドを作成しました。
@RequestMapping
今回 @RequestMapping(value = "{id}", method = RequestMethod.GET)
のように value 属性が登場しました。
value は相対パスを指定することができます。なので getTask メソッドは GET "api/tasks/{id}" とマッピングされます。
また value に指定した {id} はメソッドの引数で取得することができます。
取得するためには @PathVariable
アノテーションを付与した 変数を定義します。
@ResponseStatus
ResponseStatus アノテーションは名前の通りですが、レスポンスのステータスを指定することができます。
アノテーションに指定できる値は HttpStatus 型の値です。
HttpStatus は enum となっているので、HttpStatus.CREATED
など視覚的にわかりやすい形で指定できます。
@RequestBody
メソッドの引数に指定されている RequestBody アノテーションですが、
こちらも名前の通りリクエストボディの値をマッピングするためのアノテーションです。
今回、リクエストボディに設定される JSON を Task 型の値にマッピングするために使用しています。
結果
各メソッドを実行した結果をまとめました。
GET api/tasks
$ curl -v http://localhost:8080/api/tasks -X GET * Trying ::1... * Connected to localhost (::1) port 8080 (#0) > GET /api/tasks HTTP/1.1 > Host: localhost:8080 > User-Agent: curl/7.43.0 > Accept: */* > < HTTP/1.1 200 OK < Server: Apache-Coyote/1.1 < Content-Type: application/json;charset=UTF-8 < Transfer-Encoding: chunked < Date: Sat, 10 Oct 2015 23:40:31 GMT < * Connection #0 to host localhost left intact [{"id":0,"context":"sample0"},{"id":1,"context":"sample1"},{"id":2,"context":"sample2"}]
全てのタスクが返却されています。
GET api/tasks/{id}
$ curl -v http://localhost:8080/api/tasks/2 -X GET * Trying ::1... * Connected to localhost (::1) port 8080 (#0) > GET /api/tasks/2 HTTP/1.1 > Host: localhost:8080 > User-Agent: curl/7.43.0 > Accept: */* > < HTTP/1.1 200 OK < Server: Apache-Coyote/1.1 < Content-Type: application/json;charset=UTF-8 < Transfer-Encoding: chunked < Date: Sat, 10 Oct 2015 23:41:31 GMT < * Connection #0 to host localhost left intact {"id":2,"context":"sample2"}
id で指定したデータが返却されています。
POST api/tasks
$ curl -v http://localhost:8080/api/tasks -X POST -H "Content-Type: application/json" -d "{\"context\" : \"create\"}" * Trying ::1... * Connected to localhost (::1) port 8080 (#0) > POST /api/tasks HTTP/1.1 > Host: localhost:8080 > User-Agent: curl/7.43.0 > Accept: */* > Content-Type: application/json > Content-Length: 22 > * upload completely sent off: 22 out of 22 bytes < HTTP/1.1 201 Created < Server: Apache-Coyote/1.1 < Content-Type: application/json;charset=UTF-8 < Transfer-Encoding: chunked < Date: Sat, 10 Oct 2015 23:42:14 GMT < * Connection #0 to host localhost left intact {"id":3,"context":"create"}
作成したデータが返却されています。
@ResponseStatus(HttpStatus.CREATED)
を指定したのでレスポンスステータスが HTTP/1.1 201 Created
に変化しています。
PUT api/tasks/{id}
$ curl -v http://localhost:8080/api/tasks/1 -X PUT -H "Content-Type: application/json" -d "{\"context\" : \"update\"}" * Trying ::1... * Connected to localhost (::1) port 8080 (#0) > PUT /api/tasks/1 HTTP/1.1 > Host: localhost:8080 > User-Agent: curl/7.43.0 > Accept: */* > Content-Type: application/json > Content-Length: 22 > * upload completely sent off: 22 out of 22 bytes < HTTP/1.1 200 OK < Server: Apache-Coyote/1.1 < Content-Type: application/json;charset=UTF-8 < Transfer-Encoding: chunked < Date: Sat, 10 Oct 2015 23:42:40 GMT < * Connection #0 to host localhost left intact {"id":1,"context":"update"}
id で指定した要素のデータが更新されています。
DELETE api/tasks/{id}
$ curl -v http://localhost:8080/api/tasks/1 -X DELETE * Trying ::1... * Connected to localhost (::1) port 8080 (#0) > DELETE /api/tasks/1 HTTP/1.1 > Host: localhost:8080 > User-Agent: curl/7.43.0 > Accept: */* > < HTTP/1.1 204 No Content < Server: Apache-Coyote/1.1 < Date: Sat, 10 Oct 2015 23:42:56 GMT < * Connection #0 to host localhost left intact
@ResponseStatus(HttpStatus.NO_CONTENT)
を指定したので HTTP/1.1 204 No Content
となり
レスポンスもありません。
以上で REST API の作成は完了です。
次回はモック部分のデータを DB とやりとりするよう修正したいと思います。