LoginSignup
15
1

More than 1 year has passed since last update.

フロントエンド/バックエンドを分離して開発するためにHoverflyを利用してみる

Last updated at Posted at 2021-12-18

はじめに

以下の記事で『HTTP(S)トラフィック検証用のAPIサーバを即座に提供するOSS』と題してHoverflyを紹介しました.

本記事では,Hoverflyの活用例をご紹介したいと思います.
今回例として作成としたアプリは,HTTPを利用して通信を行います.
APIサーバはNode.jsとMySQLによって作成されており,Pyhonを利用するClientからのrequestに応じてresponseを返します.

スライド0.PNG

このAPIサーバについて,Hoverflyを用いてテスト用APIサーバに差し替えます.

スライド1.PNG

次の開発の流れに沿って順に説明をしていきます.

  1. バックエンドを用意する.(通信内容の決定,作成,疎通確認)
  2. バックエンドをモック化して,フロントエンドとバックエンドを完全に切り離す.
  3. フロントエンド,バックエンドそれぞれの開発を進める.

動作確認環境

名称 バージョン
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を取得します.
途中,必要に応じてSimulationssimulation.jsonにexportし,内容を編集してimportします.

通信内容

本記事で取り扱うアプリは次のようなデータ(JSON)のやり取りを行います.

通信.png

Clientからは,ユーザ情報({"user":"太郎"}など)をリクエストパラメータに添えてrequestを送ります.
DB(MySQL)には生物の分類(界門網目科)情報が格納されています.
Node.jsはユーザ情報(ユーザ名)から日本語か英語かどちらで返答するべきかを判定し,DBに格納されている表から分類情報をランダムに取得します.
さらに,取得した分類情報({'id': 1, 'name': 'ハナアブ', 'kingdom': '動物界', 'phylum': '節足動物門', 'class': '昆虫綱', 'order': 'ハエ目', 'family': 'ハナアブ科'}など)をレスポンスボディに含めて返します.

Hoverflyを導入した後は次のようなやり取りになります.

通信Hov.png

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を書きます.

apiserver.js
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によってモックを使って処理をする仕組みです.

hover_test.py
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 証明書の失効チェックを無効にすると実行できます.

15
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
1