Closed4
[runn] 複数API/リクエストを組み合わせた良い感じのバックエンドテスト設計方法
今までの課題
この手の設計で有名なのが「Postman(Newman)」ですが、GUIで全てをやろうとさせすぎて「自由度がなさすぎ」問題に直面しまくってました。
例えば以下のような場合
- 複数API/リクエストを組み合わせて、例えば「作成→更新→削除」の流れをレスポンス値を使って上手に設計したい
- シナリオ自体を「コンパクト」にしたい
単一APIを手軽に検証する場合は良いのですが、複数APIを連携させてテストシナリオを組む場合、非常に大きな労力が必要とすることが問題でした。
そこで以下の解決策。
解決策
- 結論: runn がめちゃ良い
せっかちな方向け
Github Actionsのテンプレート を用意したのでそちらを見ると何となく使い方が分かるかと思います。
使ってみる
- インストール:
brew install k1LoW/tap/runn
- 適当にシナリオ作成
example.yml
desc: Echo HelloWorld
runners:
req: https://postman-echo.com/
vars:
key: value
steps:
-
req:
/get?foo1={{ vars.key }}:
get:
body:
application/json:
test: steps[0].res.status == 200
- デバッグモードで実行:
$ runn run example.yml --debug
Run 'req' on 'Echo HelloWorld'.steps[0]
-----START HTTP REQUEST-----
GET /get?foo1=value HTTP/1.1
Host: postman-echo.com
Content-Type: application/json
-----END HTTP REQUEST-----
-----START HTTP RESPONSE-----
HTTP/2.0 200 OK
Content-Length: 318
...
{"args":{"foo1":"value"},"headers":{"x-forwarded-proto":"https","x-forwarded-port":"443","host":"postman-echo.com","x-amzn-trace-id":"Root=1-632aa21a-2f66acd6050acb4443d4537f","content-type":"application/json","accept-encoding":"gzip","user-agent":"Go-http-client/2.0"},"url":"https://postman-echo.com/get?foo1=value"}
-----END HTTP RESPONSE-----
Run 'test' on 'Echo HelloWorld'.steps[0]
-----START TEST CONDITION-----
steps[0].res.status == 200
├── steps[0].res.status => 200
└── 200 => 200
-----END TEST CONDITION-----
Echo HelloWorld ... ok
1 scenario, 0 skipped, 0 failures
どんな値が返ってきてるのかはtest
部分を以下のように変更。
test: steps != null
$ runn run example.yml --debug
...
Run 'test' on 'Echo HelloWorld'.steps[0]
-----START TEST CONDITION-----
steps != null
├── steps => [{"res":{"body":{"args":{"foo1":"value"},"headers":{"accept-encoding":"gzip","content-type":"application/json","host":"postman-echo.com","user-agent":"Go-http-client/2.0","x-amzn-trace-id":"Root=1-632aa502-09b8f95026b92efd3b5e1935","x-forwarded-port":"443","x-forwarded-proto":"https"},"url":"https://postman-echo.com/get?foo1=value"},"headers":{"Content-Length":["318"],"Content-Type":["application/json; charset=utf-8"],"Date":["Wed, 21 Sep 2022 05:45:38 GMT"],"Etag":["W/\"13e-E6dyeWR7YLqi6CQIuVbReAD9RgQ\""],"Set-Cookie":["sails.sid=s%3A9Wy39EBaKj3bkjAVDM9-kSQDHYb2p2z_.p0H31WUEQXW5liToVfUfli5b%2BTVAD3lGxAtoPSTX3jg; Path=/; HttpOnly"],"Vary":["Accept-Encoding"]},"rawBody":"{\"args\":{\"foo1\":\"value\"},\"headers\":{\"x-forwarded-proto\":\"https\",\"x-forwarded-port\":\"443\",\"host\":\"postman-echo.com\",\"x-amzn-trace-id\":\"Root=1-632aa502-09b8f95026b92efd3b5e1935\",\"content-type\":\"application/json\",\"accept-encoding\":\"gzip\",\"user-agent\":\"Go-http-client/2.0\"},\"url\":\"https://postman-echo.com/get?foo1=value\"}","status":200}}]
└── null => null
-----END TEST CONDITION-----
おまけ
返ってきた値を「JSON Viewer」にかけると良い感じに見れます!
使い方テクニック集
crudPostman.yml
desc: Http Request Examples on Postman
runners:
req: https://postman-echo.com/
vars:
foo1: bar1
foo2: bar2
base64Auth: "cG9zdG1hbjpwYXNzd29yZA==" # echo -n "postman:password"|base64
steps:
read:
req:
/get?foo1={{ vars.foo1 }}:
get:
body: null
test: |
steps.read.res.status == 200 &&
steps.read.res.body.args.foo1 == vars.foo1
update:
req:
/put:
put:
body:
application/json:
{
"foo1": "{{ vars.foo1 }}",
"foo2": "{{ vars.foo2 }}",
}
test: steps.update.res.status == 200
test: steps.update.res.body.data.foo1 == vars.foo1
patch:
req:
/patch:
patch:
body:
application/json:
{
"foo1": "{{ steps.update.res.body.url }}",
"foo2": "{{ vars.foo2 }}",
}
test: |
steps.patch.res.status == 200 &&
steps.patch.res.body.data.foo1 == steps.update.res.body.url
basicAuth:
req:
/basic-auth:
get:
headers:
"Authorization": "Basic {{vars.base64Auth}}"
body: null
test: |
steps.basicAuth.res.status == 200 &&
steps.basicAuth.res.body.authenticated == true
- 実行結果
$ runn run crudPostman.yml --debug
Run 'req' on 'Http Request Examples on Postman'.steps.read
-----START HTTP REQUEST-----
GET /get?foo1=bar1 HTTP/1.1
Host: postman-echo.com
-----END HTTP REQUEST-----
-----START HTTP RESPONSE-----
HTTP/2.0 200 OK
Content-Length: 282
Content-Type: application/json; charset=utf-8
Date: Wed, 21 Sep 2022 08:35:23 GMT
Etag: W/"11a-OAq82ELVnk04SdTiLtXGa9gqsg0"
Set-Cookie: sails.sid=s%3AoJOwBncXUT3DyJyka35COv0U9Sq3VO9I.6UAOxnALzzeOmvAbrMna2ufl3ymaZLobDtL5x5k7PxU; Path=/; HttpOnly
Vary: Accept-Encoding
{"args":{"foo1":"bar1"},"headers":{"x-forwarded-proto":"https","x-forwarded-port":"443","host":"postman-echo.com","x-amzn-trace-id":"Root=1-632acccb-0e8c7f254eaa4e4628242c84","accept-encoding":"gzip","user-agent":"Go-http-client/2.0"},"url":"https://postman-echo.com/get?foo1=bar1"}
-----END HTTP RESPONSE-----
Run 'test' on 'Http Request Examples on Postman'.steps.read
-----START TEST CONDITION-----
steps.read.res.status == 200 &&
steps.read.res.body.args.foo1 == vars.foo1
├── steps.read.res.status => 200
├── 200 => 200
├── steps.read.res.body.args.foo1 => "bar1"
└── vars.foo1 => "bar1"
-----END TEST CONDITION-----
Run 'req' on 'Http Request Examples on Postman'.steps.update
-----START HTTP REQUEST-----
PUT /put HTTP/1.1
Host: postman-echo.com
Content-Type: application/json
{"foo1":"bar1","foo2":"bar2"}
-----END HTTP REQUEST-----
-----START HTTP RESPONSE-----
HTTP/2.0 200 OK
Content-Length: 410
Content-Type: application/json; charset=utf-8
Date: Wed, 21 Sep 2022 08:35:23 GMT
Etag: W/"19a-DQ3n1VofQehnWNrvUAwpEGlLjXo"
Set-Cookie: sails.sid=s%3AR727vJfge831BfzW6MRmkulHYrHKK7u5.O2dp%2FuEinuq9OG2rH9JuOFUFNJHcfHkXQHrOrxGswqI; Path=/; HttpOnly
Vary: Accept-Encoding
{"args":{},"data":{"foo1":"bar1","foo2":"bar2"},"files":{},"form":{},"headers":{"x-forwarded-proto":"https","x-forwarded-port":"443","host":"postman-echo.com","x-amzn-trace-id":"Root=1-632acccb-124c21924b21a0144e5a7b9d","content-length":"29","content-type":"application/json","accept-encoding":"gzip","user-agent":"Go-http-client/2.0"},"json":{"foo1":"bar1","foo2":"bar2"},"url":"https://postman-echo.com/put"}
-----END HTTP RESPONSE-----
Run 'test' on 'Http Request Examples on Postman'.steps.update
-----START TEST CONDITION-----
steps.update.res.body.data.foo1 == vars.foo1
├── steps.update.res.body.data.foo1 => "bar1"
└── vars.foo1 => "bar1"
-----END TEST CONDITION-----
Run 'req' on 'Http Request Examples on Postman'.steps.patch
-----START HTTP REQUEST-----
PATCH /patch HTTP/1.1
Host: postman-echo.com
Content-Type: application/json
{"foo1":"https://postman-echo.com/put","foo2":"bar2"}
-----END HTTP REQUEST-----
-----START HTTP RESPONSE-----
HTTP/2.0 200 OK
Content-Length: 460
Content-Type: application/json; charset=utf-8
Date: Wed, 21 Sep 2022 08:35:24 GMT
Etag: W/"1cc-76AMb1ReR2/pqZAdLjohK30/k+Y"
Set-Cookie: sails.sid=s%3ASLhMykF9MkTy7GVbaxSkIeAmVuhv0ZYq.bBbgpZ4m%2BMpgYzMcKI3sc9M7OLZOJ5%2BGKMDhiROvC0w; Path=/; HttpOnly
Vary: Accept-Encoding
{"args":{},"data":{"foo1":"https://postman-echo.com/put","foo2":"bar2"},"files":{},"form":{},"headers":{"x-forwarded-proto":"https","x-forwarded-port":"443","host":"postman-echo.com","x-amzn-trace-id":"Root=1-632acccc-4bccc7000da4ab7360c3321f","content-length":"53","content-type":"application/json","accept-encoding":"gzip","user-agent":"Go-http-client/2.0"},"json":{"foo1":"https://postman-echo.com/put","foo2":"bar2"},"url":"https://postman-echo.com/patch"}
-----END HTTP RESPONSE-----
Run 'test' on 'Http Request Examples on Postman'.steps.patch
-----START TEST CONDITION-----
steps.patch.res.status == 200 &&
steps.patch.res.body.data.foo1 == steps.update.res.body.url
├── steps.patch.res.status => 200
├── 200 => 200
├── steps.patch.res.body.data.foo1 => "https://postman-echo.com/put"
└── steps.update.res.body.url => "https://postman-echo.com/put"
-----END TEST CONDITION-----
Run 'req' on 'Http Request Examples on Postman'.steps.basicAuth
-----START HTTP REQUEST-----
GET /basic-auth HTTP/1.1
Host: postman-echo.com
Authorization: Basic cG9zdG1hbjpwYXNzd29yZA==
-----END HTTP REQUEST-----
-----START HTTP RESPONSE-----
HTTP/2.0 200 OK
Content-Length: 22
Content-Type: application/json; charset=utf-8
Date: Wed, 21 Sep 2022 08:35:24 GMT
Etag: W/"16-sJz8uwjdDv0wvm7//BYdNw8vMbU"
Set-Cookie: sails.sid=s%3A1nM53zEvDvlb-vX-Y0GfkO70UhdRLugT.NoccZPXGqFEi46QkLGP%2F1tEiUAHWBdkTAwLb1CPAqwE; Path=/; HttpOnly
Vary: Accept-Encoding
{"authenticated":true}
-----END HTTP RESPONSE-----
Run 'test' on 'Http Request Examples on Postman'.steps.basicAuth
-----START TEST CONDITION-----
steps.basicAuth.res.status == 200 &&
steps.basicAuth.res.body.authenticated == true
├── steps.basicAuth.res.status => 200
├── 200 => 200
├── steps.basicAuth.res.body.authenticated => true
└── true => true
-----END TEST CONDITION-----
Http Request Examples on Postman ... ok
1 scenario, 0 skipped, 0 failures
このスクラップは2022/09/21にクローズされました