Frisby.js v2.xを使用したWeb APIテスト

# v2.xでの変更点

[Upgrading From Frisby 0.x to 2.x](https://github.com/vlucas/frisby/wiki/Upgrading#upgrading-from-frisby-0x-to-2x)

- Frisby.jsでJasmine Test Frameworkを隠蔽しない
- Jasmine Testの実行環境を[jasmine-node](https://www.npmjs.com/package/jasmine-node)から[Jest](https://www.npmjs.com/package/jest)に変更
- JSON Validatorに[joi](https://www.npmjs.com/package/joi)を採用
- JSON Schemaは未サポート(使用したい場合は[こちら](#custom-expect-handlers
)を参照)

細かいところでは…

- HTTP通信を[request](https://www.npmjs.com/package/request)から[node-fetch](https://www.npmjs.com/package/node-fetch)に変更
- 使用するJavaScriptライブラリを[Underscore.js](https://www.npmjs.com/package/underscore)から[Lodash](https://www.npmjs.com/package/lodash)に変更

# 環境

## パッケージ

**Frisby.js**
[![NPM](https://nodei.co/npm/frisby.png)](https://nodei.co/npm/frisby/)
https://www.npmjs.com/package/frisby
https://docs.frisbyjs.com/

**Jest**
[![NPM](https://nodei.co/npm/jest.png)](https://nodei.co/npm/jest/)
https://www.npmjs.com/package/jest
http://facebook.github.io/jest/

## 環境構築
```
$ npm install frisby
$ npm install jest
```

# テスト作成

## 記述

```js:__tests__/*.js
beforeAll(() => {
  frisby.globalSetup({
    // 共通設定
  });
});

describe('Describe', () => {
  it('Test', () => {
    // FrisbySpecでテストを実施する
    return frisby.get('/users/1')
      // FrisbySpecでチェーンして色々準備する…
  });
});
```

## Frisby

|メソッド||
|:--|:--|
|formData()|FormDataを生成する(Multipart用)|
|~~create(name)~~|Deplicated|

|プロパティ||
|:--|:--|
|version||
|utils||
|Joi|[joiを参照する](https://www.npmjs.com/package/frisby#nested-dependent-http-calls)|

### Global setup

|メソッド||
|:--|:--|
|globalSetup(opts)|上書き<br>nullを指定すると既定値に戻る|
|baseUrl(url)|globalSetup()後に設定する|

opts

```js
{
  request: {
    credentials: 'include',

    headers: {
      // 既定値
      'Content-Type': 'application/json',
      'User-Agent': 'frisby/2.1.2 (+https://github.com/vlucas/frisby)',
    },

    // baseUrl(既定値:undefined)
    baseUrl: undefined,

    // rawBody(既定値:false)
    rawBody: false,

    // inspectOnFailure(既定値:true)
    inspectOnFailure: true,

    // timeout(既定値:5000[ms])
    timeout: 5000,

    // redirect('follow'または'manual' 既定値:'follow')
    redirect: 'follow',
  },
};

```

### FrisbySpec

|メソッド||
|:--|:--|
|get()||
|head()||
|options()||
|patch()||
|post()||
|put()||
|delete()|= del()|
|fetch()||
|use()||
|setup()||
|timeout()||
|fromJSON()||

FrisbySpecを返却する

### Custom Expect Handlers

[Define Custom Expect Handlers](https://www.npmjs.com/package/frisby#define-custom-expect-handlers)

|メソッド||
|:--|:--|
|addExpectHandler(expectName, expectFn)||
|removeExpectHandler(expectName)||

```js:サンプル(HTTPステータスがどれかに一致するかどうか)
const frisby = require('frisby');

beforeAll(() => {
  frisby.addExpectHandler('statuses', (response, statuses) => {
    if (Array.isArray(statuses)) {
      expect(statuses).toContain(response.status);
    } else {
      expect(response.status).toBe(statuses);
    }
  });
});

afterAll(() => {
  // 必要であれば削除する
  frisby.removeExpectHandler('statuses');
});

describe('Custom Expect Handler', () => {
  it('Test', () => {
    return frisby.get('https://www.google.com')
      .expect('statuses', [200, 301])
      .expect('statuses', 200);
  });
});
```

```js:サンプル(JSON&nbsp;Schemaによるvalidation)
const frisby = require('frisby');
const Validator = require('jsonschema').Validator; // install jsonschema.

beforeAll(() => {
  frisby.addExpectHandler('jsonschema', (response, _path, _schema) => {
    let schema = _schema === undefined ? _path : _schema;
    let path = _schema === undefined ? false : _path;

    frisby.utils.withPath(path, response.json, jsonChunk => {
      let validator = new Validator();
      validator.validate(jsonChunk, schema, { throwError: true });
    });
  });
});

afterAll(() => {
  // 必要であれば削除する
  frisby.removeExpectHandler('jsonschema');
});

describe('Custom Expect Handler', () => {
  it('Test1', () => {
    return frisby.fromJSON(4)
      .expect('jsonschema', { 'type': 'number'});
  });
  it('Test2', () => {
    return frisby.fromJSON([0, 1, 2, 3, 4])
      .expect('jsonschema', '*', { 'type': 'number'});
  });
});
```

## FrisbySpec

- メソッドはFrisbySpecを返却する(チェーンが可能)
- getterはuse()を使用する

|メソッド||
|:--|:--|
|use(fn)|チェーン用|
|setup(opts, replace)|通常はglobalSetup()で設定する<br>replaceがtrueの場合は上書き|
|timeout(timeout)|getter/setter|
|getBaseUrl()||

### HTTP Requests

|メソッド||
|:--|:--|
|get(url, params)||
|head(url, params)||
|options(url, params)||
|patch(url, params)||
|post(url, params)||
|put(url, params)||
|delete(url, params)|= del(url, params)|
|fetch(url, params = {}, options = {})|[node-fetch](https://www.npmjs.com/package/node-fetch) API用|
|fromJSON(json)|ローカル用(jsonで指定した値がResponseとなる)|

```js:HTTP&nbsp;HEADで要求する場合
const frisby = require('frisby');

describe('HTTP method', () => {
  it('HEAD', () => {
    return frisby.fetch('http://qiita.com/', {
      method: 'HEAD',
      compress: false,
    })
      .expect('status', 200);
  });
});
```

```js:HTTP&nbsp;GET(URLパラメーターあり)で要求する場合
const frisby = require('frisby');

describe('HTTP method', () => {
  it('GET', () => {
    const url = new URL('https://www.google.com/search');
    url.searchParams.append('q', 'http://qiita.com/');
    return frisby.fetch(url.href, {
      method: 'GET',
    }, {
      urlEncode: false,
    })
      .expect('status', 200);
  });
});
```

### Expectations

|メソッド||
|:--|:--|
|expect(expectName)||
|expectNot(expectName)||

[Built-In Expect Handlers](https://www.npmjs.com/package/frisby#built-in-expect-handlers)

|expectName|引数||
|:--|:--|:--|
|status|statusCode||
|header|header, [headerValue]||
|json|[path], json|指定した値のみを検証|
|jsonStrict|[path], json|すべての値を検証|
|jsonTypes|[path], json|指定した型のみを検証|
|jsonTypesStrict|[path], json|すべての型を検証|
|bodyContains|value|Bodyがテキストの場合|

#### path

- Arrayの要素は数字または角括弧で指定
- pathの値が存在しない場合はError

|特殊文字||||
|:--|:--|:--|:--|
|*|Array, Object|すべての要素が対象|= &|
|?|Array, Object|どれかひとつの要素が対象||

### Inspectors

|メソッド||
|:--|:--|
|inspectRequest()||
|inspectRequestHeaders()||
|inspectResponse()||
|inspectHeaders()||
|inspectStatus()||
|inspectBody()||
|inspectJSON()||
|inspectLog()|即時出力|

### Promise

|メソッド||
|:--|:--|
|then(fn)||
|catch(fn)||
|finally(fn)||
|done(doneFn)|※最近のトレンドはFrisbySpec(= Promise)を返却する形式|
|promise()|[node-fetch](https://www.npmjs.com/package/node-fetch)のPromiseが返却される|

## Tips

### Form形式の場合

v2.xはJSON形式が前提(jsonパラメーターがなくなった)だが、以下のようにすることでv.0.8.5から多少の変更で送信が可能

```js:application/x-www-form-urlencoded
const frisby = require('frisby');
const qs = require('qs'); // Jestで入る

describe('application/x-www-form-urlencoded', () => {
  it('Test', () => {
    return frisby.post('/login', {
      body: qs.stringify({
        id: 'testy.mctesterpants@example.com',
        password: 'frisbbbbbbbbbby',
      }),
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
      }
    })
      .expect('status', 200);
  });
});
```

```js:multipart/form-data
const frisby = require('frisby');

describe('multipart/form-data', () => {
  it('Test', () => {
    let form = frisby.formData();
    form.append('id', 'testy.mctesterpants@example.com');
    form.append('password', 'frisbbbbbbbbbby');

    return frisby.post('/login', {
      body: form
    })
      .expect('status', 200);
  });
});
```

### Basic認証

```js:user&#x3A;&nbsp;user,&nbsp;password&#x3A;&nbsp;passwd
const frisby = require('frisby');
const url = require('url');

it('auth test(2)', () => {
  // use Legacy URL API.
  let myURL = url.parse('https://httpbin.org/basic-auth/user/passwd');
  myURL.auth = 'user:passwd';
  return frisby.get(url.format(myURL))
    .expect('status', 200);
});
```

### バイナリのテスト

```js:PNG解析
const frisby = require('frisby');

it('PNG test', () => {
  return frisby.setup({ request: { rawBody: true } })
    .get('https://httpbin.org/image/png')
    .expect('status', 200)
    .expect('header', 'Content-Type', 'image/png')
    .then(res => {
      let body = res.body;
      expect(body.byteLength).toBeGreaterThan(8);
      expect(String.fromCharCode.apply(null, new Uint8Array(body.slice(1, 4)))).toEqual('PNG');
    });
});
```

# テスト実施

## 準備

```json:package.json
{
  "scripts": {
    "test": "jest"
  }
}
```

オプション
[Configuring Jest](https://facebook.github.io/jest/docs/en/configuration.html)

## 実行

1. すべて実行する場合<br>
```$ npm test```
2. ファイルを指定して実行する場合<br>
```$ npm test -- __tests__/xxx.spec.js```

※Windows(Git Bash)でファイル名を指定して実行する場合のメモ
```$ npm test -- __tests__\\\\xxx.spec.js```