はじめに
以下の記事で『HTTP(S)トラフィック検証用のAPIサーバを即座に提供するOSS』と題してHoverflyを紹介しました.
本記事では,Hoverflyの活用例をご紹介したいと思います.
今回例として作成としたアプリは,HTTPを利用して通信を行います.
APIサーバはNode.jsとMySQLによって作成されており,Pyhonを利用するClientからのrequestに応じてresponseを返します.
このAPIサーバについて,Hoverflyを用いてテスト用APIサーバに差し替えます.
次の開発の流れに沿って順に説明をしていきます.
- バックエンドを用意する.(通信内容の決定,作成,疎通確認)
- バックエンドをモック化して,フロントエンドとバックエンドを完全に切り離す.
- フロントエンド,バックエンドそれぞれの開発を進める.
動作確認環境
名称 | バージョン |
---|---|
Windows 10 | 21H2 |
Hoverfly | 1.3.0 |
hoverctl | master-3578 |
Python | 3.10.0 |
Node.js | 14.18.1 |
express | 4.17.1 |
npm | 6.14.15 |
MySQL Comminity | 8.0.27 |
Hoverflyは以下のOSに対応しています.
- MacOS 64bit
- Linux 32bit/64bit
- Windows 32bit/64bit
本記事では,Hoverfly公式1でも記述が少ないWindowsを利用した例を示します.
Node.js, express, MySQLによるAPIサーバの立て方については様々な記事2があるので詳細は割愛させていただきます.
requestの送信にはPythonを利用しました.
インストール
Hoverfly
公式ページ3からダウンロードします.
zipファイルを解凍し,『システムの詳細設定』=>『環境変数』から解凍したフォルダにPathを通せば完了です.
hoverctl
は,Hoverfly APIとローカルのファイルシステム間の連携設定やHoverflyの実行を担うコマンドラインツールです.
hoverfly
はアプリケーション本体で,プロキシサーバやウェブサーバ,APIエンドポイントを提供します.
インストールされたことは以下のコマンドで確認できます.
hoverfly -version
> v1.3.0
hoverctl version
> +----------+-------------+
> | hoverctl | master-3578 |
> | hoverfly | v1.3.0 |
> +----------+-------------+
Python
こちらも公式ページ4からインストーラを取得してインストールを行います.
インストール後,PythonのPathが通っていることも確認します.
request送信時に利用するrequests
, json
パッケージもインストールしておきます.
python --version
> Python 3.10.0
pip install requests
pip install json
Node.js, express
Node.jsも公式ページ5から取得します.
APIサーバの作成で利用するexpress
, body-parser
, cors
などをnpm
を使ってインストールしておきます.
node -v
> v14.18.1
npm init # 環境の依存関係を記述するpackage.jsonを作成します.
npm install express --save
npm install body-parser --save
npm install cors --save
npm install mysql --save # DBとの接続で利用します.
# package.jsonを見るか,次のコマンドを打ってインストールしたパッケージを確認します.
npm ls
MySQL
MySQLのインストーラも公式6から取得できます.
本記事ではCommunity Serverを利用しました.
Hoverflyを利用した開発
テスト用APIサーバを作成すると,フロントエンドの開発担当者はテスト用APIサーバにrequestを送りながらフロントエンド開発を行い,バックエンド担当者は独立してDB変更やデータ追加などのバックエンド開発を行うことが容易になります.
このように役割を明確に分けておくことで,統合時の衝突を避けることができます.
そこで,Hoverflyの『HTTP(S)を利用する既存のサービスについて簡単にテスト用APIサーバを作成できる』機能を活用したいと考えました.
本記事では,ランダムに昆虫の情報を返してくれる"昆虫図鑑アプリ"を題材に使ってみたいと思います.
また,Hoverflyにはランダムな返答の順番も再現したモックを作成してほしい場合を考えます.
まずHoverflyをCapture modeで起動し,requestと返されたresponseを元にSimulations
を構成します.
次にHoverflyをSimulate modeで起動し,Clientがテスト用APIサーバにrequestを送ることでSimulations
に基づいたresponseを取得します.
途中,必要に応じてSimulations
をsimulation.json
にexportし,内容を編集してimportします.
通信内容
本記事で取り扱うアプリは次のようなデータ(JSON)のやり取りを行います.
Clientからは,ユーザ情報({"user":"太郎"}
など)をリクエストパラメータに添えてrequestを送ります.
DB(MySQL)には生物の分類(界門網目科)情報が格納されています.
Node.jsはユーザ情報(ユーザ名)から日本語か英語かどちらで返答するべきかを判定し,DBに格納されている表から分類情報をランダムに取得します.
さらに,取得した分類情報({'id': 1, 'name': 'ハナアブ', 'kingdom': '動物界', 'phylum': '節足動物門', 'class': '昆虫綱', 'order': 'ハエ目', 'family': 'ハナアブ科'}
など)をレスポンスボディに含めて返します.
Hoverflyを導入した後は次のようなやり取りになります.
Hoverflyを用いてモック化することで,バックエンドを意識することなくフロントの開発をできるようになっていることがわかると思います.
APIサーバの構築
MySQLでhoverdbデータベースを作成し,日本語版と英語版の表を格納します.
mysql> select * from testJPNTable;
+--------+----------------+-------------+------------+-----------+-----------+--------------+
| ID_JPN | NAME_JPN | KINGDOM_JPN | PHYLUM_JPN | CLASS_JPN | ORDER_JPN | FAMILY_JPN |
+--------+----------------+-------------+------------+-----------+-----------+--------------+
| 1 | ハナアブ | 動物界 | 節足動物門 | 昆虫綱 | ハエ目 | ハナアブ科 |
| 2 | カブトムシ | 動物界 | 節足動物門 | 昆虫綱 | 甲虫目 | コガネムシ科 |
| 3 | ミツバチ | 動物界 | 節足動物門 | 昆虫綱 | ハチ目 | ミツバチ科 |
| 4 | アキアカネ | 動物界 | 節足動物門 | 昆虫綱 | トンボ目 | トンボ科 |
| 5 | トノサマバッタ | 動物界 | 節足動物門 | 昆虫綱 | バッタ目 | バッタ科 |
+--------+----------------+-------------+------------+-----------+-----------+--------------+
mysql> select * from testENGTable;
+--------+----------------------------+-------------+------------+-----------+-------------+--------------+
| ID_ENG | NAME_ENG | KINGDOM_ENG | PHYLUM_ENG | CLASS_ENG | ORDER_ENG | FAMILY_ENG |
+--------+----------------------------+-------------+------------+-----------+-------------+--------------+
| 1 | Hover fly | Animalia | Arthropoda | Insecta | Diptera | Syrphidae |
| 2 | Japanese rhinoceros beetle | Animalia | Arthropoda | Insecta | Coleoptera | Scarabaeidae |
| 3 | Honey bee | Animalia | Arthropoda | Insecta | Hymenoptera | Apidae |
| 4 | Sympetrum frequens | Animalia | Arthropoda | Insecta | Odonata | Libellulidae |
| 5 | Migratory locust | Animalia | Arthropoda | Insecta | Orthoptera | Acrididae |
+--------+----------------------------+-------------+------------+-----------+-------------+--------------+
requestに応じて接続したDBからランダムにデータを取得して返答するように,以下のJavaScriptを書きます.
const exp = require("express");
const bodyParser = require('body-parser');
const cors = require('cors');
const app = exp();
app.use(bodyParser.urlencoded({extended: true}));
app.use(bodyParser.json());
app.use(cors());
const sql = require("mysql");
const con = sql.createConnection({
user : "root",
host : "localhost",
password : "password",
database : "hoverdb"
})
app.listen(3100,function(){
console.log("Start DB-API Server.");
})
app.post("/test",function(req,res,nex){
let data = {};
let user = req.body.user;
let dataNumAll;
let dataNum;
let lang = "ENG"
new Promise(resolve => {
for(var i=0; i < user.length; i++){
if(user.charCodeAt(i) >= 256) {
lang = "JPN";
break;
};
};
resolve();
}).then(() => {
if (lang == "JPN"){
return new Promise(resolve => {
con.query("select count(ID_JPN) as dataNumAll from testJPNTable", async (e, r) => {
if (e) throw e;
dataNumAll = r[0].dataNumAll;
resolve();
});
}).then(() => {
return new Promise(resolve => {
dataNum = Math.floor(Math.random() * dataNumAll) + 1;
resolve();
});
}).then(() => {
return new Promise(resolve => {
con.query("select ID_JPN, NAME_JPN, KINGDOM_JPN, PHYLUM_JPN, CLASS_JPN, ORDER_JPN, FAMILY_JPN from testJPNTable where ID_JPN = ?", [dataNum], async (e,r) => {
if(e) throw e;
data["id"] = r[0].ID_JPN;
data["name"] = r[0].NAME_JPN;
data["kingdom"] = r[0].KINGDOM_JPN;
data["phylum"] = r[0].PHYLUM_JPN;
data["class"] = r[0].CLASS_JPN;
data["order"] = r[0].ORDER_JPN;
data["family"] = r[0].FAMILY_JPN;
resolve();
});
});
}).then(() => {
res.send(data);
});
} else {
return new Promise(resolve => {
con.query("select count(ID_ENG) as dataNumAll from testENGTable", async (e, r) => {
if (e) throw e;
dataNumAll = r[0].dataNumAll;
resolve();
});
}).then(() => {
return new Promise(resolve => {
dataNum = Math.floor(Math.random() * dataNumAll) + 1;
resolve();
});
}).then(() => {
return new Promise(resolve => {
con.query("select ID_ENG, NAME_ENG, KINGDOM_ENG, PHYLUM_ENG, CLASS_ENG, ORDER_ENG, FAMILY_ENG from testENGTable where ID_ENG = ?", [dataNum], async (e,r) => {
if(e) throw e;
data["id"] = r[0].ID_ENG;
data["name"] = r[0].NAME_ENG;
data["kingdom"] = r[0].KINGDOM_ENG;
data["phylum"] = r[0].PHYLUM_ENG;
data["class"] = r[0].CLASS_ENG;
data["order"] = r[0].ORDER_ENG;
data["family"] = r[0].FAMILY_ENG;
resolve();
});
});
}).then(() => {
res.send(data);
});
};
});
});
APIサーバが正常にデータを返してくれることを確認します.
コマンドプロンプトを2つ開き,片方でAPIサーバを起動して,もう片方からcurlをします.
node apiserver.js
> Start DB-API Server.
curl -H "Content-type: application/json" -X POST -d "{\"user\":\"John\"}" http://localhost:3100/test
> {"id":3,"name":"Honey bee","kingdom":"Animalia","phylum":"Arthropoda","class":"Insecta","order":"Hymenoptera","family":"Apidae"}
curl -H "Content-type: application/json" -X POST -d "{\"user\":\"John\"}" http://localhost:3100/test
> {"id":2,"name":"Japanese rhinoceros beetle","kingdom":"Animalia","phylum":"Arthropoda","class":"Insecta","order":"Coleoptera","family":"Scarabaeidae"}
curl -H "Content-type: application/json" -X POST -d "{\"user\":\"太郎\"}" http://localhost:3100/test
> {"id":4,"name":"アキアカネ","kingdom":"動物界","phylum":"節足動物門","class":"昆虫綱","order":"トンボ目","family":"トンボ科"}
curl -H "Content-type: application/json" -X POST -d "{\"user\":\"太郎\"}" http://localhost:3100/test
> {"id":2,"name":"カブトムシ","kingdom":"動物界","phylum":"節足動物門","class":"昆虫綱","order":"甲虫目","family":"コガネムシ科"}
バックエンドのモック化
コマンドプロンプトからSimulations
の作成を行ってみます.
まずはHoverflyインスタンスを--stateful
オプション付きのCapture modeで起動し,数回curlします.
最後にhoverctl export
コマンドでsimulation.json
を取得します.
hoverctl start
hoverctl mode capture --stateful
curl --proxy http://localhost:8500 -H "Content-type: application/json" -X POST -d "{\"user\":\"John\"}" http://localhost:3100/test
> {'id': 1, 'name': 'Hover fly', ...
curl --proxy http://localhost:8500 -H "Content-type: application/json" -X POST -d "{\"user\":\"John\"}" http://localhost:3100/test
> {'id': 4, 'name': 'Sympetrum frequens', ...
curl --proxy http://localhost:8500 -H "Content-type: application/json" -X POST -d "{\"user\":\"John\"}" http://localhost:3100/test
> {'id': 2, 'name': 'Japanese rhinoceros beetle', ...
curl --proxy http://localhost:8500 -H "Content-type: application/json" -X POST -d "{\"user\":\"太郎\"}" http://localhost:3100/test
> {'id': 1, 'name': 'ハナアブ', ...
curl --proxy http://localhost:8500 -H "Content-type: application/json" -X POST -d "{\"user\":\"太郎\"}" http://localhost:3100/test
> {'id': 3, 'name': 'ミツバチ', ...
curl --proxy http://localhost:8500 -H "Content-type: application/json" -X POST -d "{\"user\":\"太郎\"}" http://localhost:3100/test
> {'id': 4, 'name': 'アキアカネ', ...
hoverctl export simulation.json
hoverctl stop
simulation.json
の中身を確認してみます.
{
"data": {
"pairs": [
{
"request": {
"path": [
{
"matcher": "exact",
"value": "/test"
}
],
"method": [
{
"matcher": "exact",
"value": "POST"
}
],
"destination": [
{
"matcher": "exact",
"value": "localhost:3100"
}
],
"scheme": [
{
"matcher": "exact",
"value": "http"
}
],
"body": [
{
"matcher": "json",
"value": "{\"user\":\"John\"}"
}
],
"requiresState": {
"sequence:1": "1"
}
},
"response": {
"status": 200,
"body": "{\"id\":1,\"name\":\"Hover fly\",\"kingdom\":...",
"encodedBody": false,
"headers": {
"Access-Control-Allow-Origin": [
"*"
],
"Connection": [
"keep-alive"
],
"Content-Length": [
"128"
],
"Content-Type": [
"application/json; charset=utf-8"
],
"Date": [
"Wed, 15 Dec 2021 07:49:46 GMT"
],
"Etag": [
"W/\"80-WtD6XUSU8j6nuFnzq6lhrvdWZn8\""
],
"Hoverfly": [
"Was-Here"
],
"Keep-Alive": [
"timeout=5"
],
"X-Powered-By": [
"Express"
]
},
"templated": false,
"transitionsState": {
"sequence:1": "2"
}
}
},
{
"request": {
...
}
}
],
...
}
}
curlでやり取りを行ったrequest-responseにしたがってSimulations
が構成されていることがわかります.
--stateful
オプションをつけていたので,requiresState
, transitionsState
によって返答順も管理されています.
ここでは,http://hogehoge/poyopoyo
にrequestを送った場合は3秒待って404 Page not found
を返すrequest-responseペアを書き足してみます.
"request": {
"path": [
{
"matcher": "exact",
"value": "/poyopoyo"
}
],
"method": [
{
"matcher": "exact",
"value": "POST"
}
],
"destination": [
{
"matcher": "exact",
"value": "hogehoge"
}
],
"scheme": [
{
"matcher": "exact",
"value": "http"
}
],
}
"response": {
"status": 404,
"body": "Page not found",
"fixedDelay": 3000
}
}
モックを利用した開発
以上でフロントエンドとバックエンドが切り離されたので,フロントエンド開発担当者はモックを使用したテスト用APIサーバにrequestを送りながら開発を行うことができます.
Pythonでrequestを行うように以下のコードを書きます.
requestの宛先はhttp://localhost:3100/test
ですが,プロキシサーバhttp://localhost:8500
として起動したHoverflyによってモックを使って処理をする仕組みです.
import requests
import json
proxies = {
"http":"http://localhost:8500/"
}
response = requests.post('http://localhost:3100/test', json = {"user" : "太郎"}, proxies=proxies)
print("1回目(日本語)")
print(response.json())
print("")
response = requests.post('http://localhost:3100/test', json = {"user" : "John"}, proxies=proxies)
print("1回目(英語)")
print(response.json())
print("")
response = requests.post('http://localhost:3100/test', json = {"user" : "太郎"}, proxies=proxies)
print("2回目(日本語)")
print(response.json())
print("")
response = requests.post('http://localhost:3100/test', json = {"user" : "John"}, proxies=proxies)
print("2回目(英語)")
print(response.json())
print("")
response = requests.post('http://localhost:3100/test', json = {"user" : "太郎"}, proxies=proxies)
print("3回目(日本語)")
print(response.json())
print("")
response = requests.post('http://localhost:3100/test', json = {"user" : "John"}, proxies=proxies)
print("3回目(英語)")
print(response.json())
print("")
response = requests.post('http://hogehoge/poyopoyo', json = {"user" : "John"}, proxies=proxies)
print("http://hogehoge/poyopoyo に送った場合")
print(response.text)
コマンドプロンプトを2つ開き,片方でテスト用APIサーバを起動して,もう片方からhover_test.py
を実行します.
hoverctl start
hoverctl mode simulation
hoverctl import simulation.json
python hover_test.py
> 1回目(日本語)
> {'id': 1, 'name': 'ハナアブ', 'kingdom': '動物界', 'phylum': '節足動物門', 'class': '昆虫綱', 'order': 'ハエ目', 'family': 'ハナアブ科'}
>
> 1回目(英語)
> {'id': 1, 'name': 'Hover fly', 'kingdom': 'Animalia', 'phylum': 'Arthropoda', 'class': 'Insecta', 'order': 'Diptera', 'family': 'Syrphidae'}
>
> 2回目(日本語)
> {'id': 3, 'name': 'ミツバチ', 'kingdom': '動物界', 'phylum': '節足動物門', 'class': '昆虫綱', 'order': 'ハチ目', 'family': 'ミツバチ科'}
>
> 2回目(英語)
> {'id': 4, 'name': 'Sympetrum frequens', 'kingdom': 'Animalia', 'phylum': 'Arthropoda', 'class': 'Insecta', 'order': 'Odonata', 'family': 'Libellulidae'}
>
> 3回目(日本語)
> {'id': 4, 'name': 'アキアカネ', 'kingdom': '動物界', 'phylum': '節足動物門', 'class': '昆虫綱', 'order': 'トンボ目', 'family': 'トンボ科'}
>
> 3回目(英語)
> {'id': 2, 'name': 'Japanese rhinoceros beetle', 'kingdom': 'Animalia', 'phylum': 'Arthropoda', 'class': 'Insecta', 'order': 'Coleoptera', 'family': 'Scarabaeidae'}
>
> http://hogehoge/poyopoyo に送った場合
> Page not found
バックエンドのモック化 で実行した順にresponseが返ってきていることがわかります.
あとは,DjangoやFlaskなどを使ってフロント開発を進めればokです.
おわりに
本記事では,Node.js + MySQLで作成したAPIサーバに対してHoverflyを利用してテスト用APIサーバを作成しました.
また,テスト用APIサーバを利用することでフロントエンドとバックエンドを明確に切り分けて開発する方法を考えてみました.
今回ご紹介した方法は,『まず始めにバックエンドの準備をすること』を前提にしていました.
Hoverflyの優れている点は既存のバックエンドを迅速にモック化してテスト用APIサーバとして開発環境を提供することですので,改修という側面が強く出ます.
よって,既存のバックエンドサービスを利用しながらフロントを改修することを主眼にご紹介しました.
一方で完全な新規開発を行いたい場合は,Capture modeを利用することなくSimulations
を全て手書きすることでモックを作成してテスト用APIサーバを利用すればよいかと思います.
その場合は,JSON Server7などと比較して利用することになると思います.
ぜひ今後の参考にしてみてください!
(オマケ)会社などのプロキシ環境下でHoverflyを使う
Hoverflyインスタンスを起動するhoverctl start
コマンドでは,--upstream-proxy
オプションを使用することで会社などのプロキシ環境下でも外部に通信できます.
# 会社などのproxy環境下利用したい場合,
hoverctl start --upstream-proxy http://my-user:my-pass@corp.proxy:8080
> Hoverfly is now running
> +------------+------+
> | admin-port | 8888 |
> | proxy-port | 8500 |
> +------------+------+
(オマケ)WindowsでHTTPSトラフィックを利用する
HTTPSトラフィックをキャプチャする場合は,Hoverfly用のSSL証明書8を使います.
公式ページにはwgetを使う方法が書かれています9が,Windowsではcurlで対応できます.
会社のプロキシ環境下で実行する際は,更に-x
オプションをつける必要があるかもしれません.
# Windowsではcurlで対応できる.-x オプションはプロキシ環境下にある場合につける.
curl https://raw.githubusercontent.com/SpectoLabs/hoverfly/master/core/cert.pem -o cert.pem -x http://my-user:my-pass@corp.proxy:8080
取得したcert.pem
を信頼されたルート証明書に登録10します.
登録が終わったら,--cacert
オプションをつけてhttpsでcurlしてみます.
hoverctl start
hoverctl mode simulate
# cert.pemを使ってhttpsでcurl.
curl --proxy localhost:8500 https://example.com --cacert cert.pem
更にcurl: (35) schannel: next InitializeSecurityContext failed: Unknown error (0x80092012)
が出る場合は,--ssl-no-revoke
オプションをつけてSSL 証明書の失効チェックを無効にすると実行できます.
-
https://younaship.com/2019/07/09/nodejs-de-api/ がとてもわかりやすかったです. ↩
-
https://docs.hoverfly.io/en/latest/pages/introduction/downloadinstallation.html ↩
-
https://docs.hoverfly.io/en/latest/pages/tutorials/advanced/configuressl/configuressl.html#configuressl ↩
-
https://docs.hoverfly.io/en/latest/pages/tutorials/basic/https/https.html ↩
-
https://jp.globalsign.com/support/faq/10.html を参考に登録できます. ↩