Skip to content

Node.js テストの書き方

Komine Shunta edited this page Sep 24, 2020 · 2 revisions

事前準備

  • 必要なものをインストールしておく
yarn add supertest chai jsdom snap-shot-it @types/supertest @types/chai @types/jsdom @types/mocha ts-mocha mocha typescript
  • appをエクスポートする
export default app
# または
module.exports = app;
  • package.jsonscripts以下にテストスクリプトを追加
"test": "NODE_ENV=test ts-mocha test.ts",
"update": "SNAPSHOT_UPDATE=1 yarn test"

テストのテンプレート

import { testOverwrites } from './testOverwrites';
import request from 'supertest';
import { assert } from 'chai';
import { JSDOM } from 'jsdom';
import snapshot from 'snap-shot-it';
import app from './app';

const agent = request.agent(app);

describe('/test', () => {
    testOverwrites(); // 外部へのリクエストや、SQLなどを監視したいときに使う
    it('should response something', async () => {
        const response = await agent
            .get('/test')
            .query({})
            .send({});
        snapshot(JSON.parse(response.text));
    });
});

外部へのリクエストのスナップショットを取る

  • overwrites.tsをコピペ
type FuncType = (...args: any) => any;

export function overwriteCallback<T, K extends keyof T>(
    self: T,
    fnName: K,
    hook: (...args: any[]) => any[]
) {
    const original = self[fnName];
    if (typeof original !== 'function') {
        throw new Error(`overwrite error: ${fnName} is not a function`);
    }

    self[fnName] = ((...args: any[]) => {
        const newArgs = args.slice(0, args.length - 1);
        const lastArg = args[args.length - 1];
        if (typeof lastArg !== 'function') {
            throw new Error(
                `overwrite error: last arg of ${fnName} is not a function`
            );
        }
        newArgs.push((...callbackArgs: any[]) => {
            const newCallbackArgs = hook(...callbackArgs);
            lastArg(...newCallbackArgs);
        });
        const result = original.apply(self, newArgs);

        return result;
    }) as any;

    return () => {
        self[fnName] = original;
    };
}

export function overwrite<T, K extends keyof T, Fn extends T[K]>(
    self: T,
    fnName: K,
    hook: Fn extends FuncType
        ? (result: ReturnType<Fn>, ...args: Parameters<Fn>) => void
        : never
) {
    if (!self) return;
    const original = self[fnName];
    if (typeof original !== 'function') return;

    self[fnName] = ((...args: any[]) => {
        const result = original.apply(self, args);
        hook(result, ...args);
        return result;
    }) as any;

    return () => {
        self[fnName] = original;
    };
}
  • testOverwrites.tsを作る
import snapshot from 'snap-shot-it';
import { overwrite, overwriteCallback } from './overwrite';

let timeline = [];

export function testOverwrites() {
    beforeEach(() => {
        timeline = [];
    });
    afterEach(() => {
        snapshot(timeline);
    });
}

node-fetchの場合

  • fetch.tsを作り、アプリケーション内でのnode-fetchimportをすべて置き換える
import fetch from 'node-fetch';
export default { fetch };
  • textOverwrites.tsに以下を追記
import nodeFetch from './fetch';

overwrite(nodeFetch, 'fetch', (result, url) => {
    timeline.push(['fetch', url]);
});

axiosの場合

  • textOverwrites.tsに以下を追記
import Axios, { AxiosResponse } from 'axios';
for (const method of ['get', 'post', 'delete', 'put']) {
    overwrite(
        Axios,
        method as any,
        (async (result: Promise<AxiosResponse<any>>, url: string) => {
            timeline.push(['axios', url, (await result).data]);
        }) as any
    );
}

superagentの場合

  • textOverwrites.tsに以下を追記
// TODO: axiosみたいにする
import agent from 'superagent';
for (const method of ['get', 'post'] as ('get' | 'post')[]) {
    overwrite(agent, method, (_, url) => {
        timeline.push(['agent', method, url]);
    });
}

requestの場合

  • textOverwrites.tsに以下を追記
// TODO: axiosみたいにする
import request from 'request';
for (const method of ['get', 'post'] as ('get' | 'post')[]) {
    overwrite(request, method, (_, url) => {
        timeline.push(['request', method, url]);
    });
}

mysqlの場合

  • textOverwrites.tsに以下を追記
import mysql from 'mysql';
overwrite(mysql, 'createPool', (pool) => {
    overwriteCallback(pool, 'getConnection', (err, connection) => {
        overwrite(connection, 'query', (_, query) => {
            timeline.push('query', query);
        });
        return [err, connection];
    });
});

mysql2の場合

  • textOverwrites.tsに以下を追記
import mysql2 from 'mysql2/promise';
overwrite(mysql2, 'createPool', (pool) => {
    overwrite(pool, 'getConnection', async (connection) => {
        overwrite(await connection, 'beginTransaction', async () => {
            timeline.push('beginTransaction');
        });
        overwrite(await connection, 'commit', async () => {
            timeline.push('commit');
        });
        overwrite(await connection, 'query', async (result, query) => {
            timeline.push('query', query);
        });
    });
});