見出し画像

GitHub CopilotでVitestのテストを書いてみたら肌感で3~4割生産性が上がった気がした話

はじめに

こんにちは。SHIFTの片山です。
今回はGitHub Copilotを利用して、ユニットテストを実装してみたいと思う。テストコードを書くのは、プロダクトのコードとの違いやそれにかかる時間から心理的な面でしんどさを感じることもあると思うが、Copilot を活用する事でどれだけ楽になるか?を確認できればと思う。

※本記事では Node.js Express の middleware に対するユニットテストを書いてみたいと思う。

GitHub Copilot の初期設定をする

詳細は他にも多くの記事が既に出ているので省く。 公式に書かれている設定を方法を確認し、GitHub の設定を行う(個人アカウントであれば、公式に記載がある通りフリートライアルができる)。

VS Code を設定する

GitHub で Copilot の設定が完了すると、以下のような画面に遷移するので、利用している IDE を選択して IDE の設定を進める。

今回は、VS Code において設定を行うが、こちらも既に多くの記事が既に出ているので、詳細については割愛する。設定手順は公式にある。

設定が完了して Copilot が動いているかは、以下のような JavaScript のコードの書きかけ途中で Tab キーを押下してサジェストが出るか?で確認できる。

Copilot を使ってvitestで Express の middleware のユニットテストを書いてみる

実際に Copilot を利用して、vitest でテストを書いてみたいと思う。

テストしたいコードは以下とする(ただし本記事で取り上げる Copilot でのテスト実装は、その中の一部のみ)。

import { strict as assert } from 'assert';
import HttpError from './custom-http-error.js';

const retriveSession = (token, store) => {
	...
};

export default () => async (req, res, next) => {
	const { sessionStore: store } = req;

	try {
		const { authorization } = req.headers;
		if (!authorization)
			throw new HttpError(401, `authorization header missing`);
		const [type, token] = authorization.split(/\s/, 2);
		if (type !== 'Bearer')
			throw new HttpError(400, `invalid token type: ${type}`);
		if (!token) throw new HttpError(401, `token missing`);

		const session = await retriveSession(token, store);
		if (!session || !session.userId)
			throw new HttpError(401, `invalid session`);

		req.token = { userId: session.userId || null };
		next();
	} catch (e) {
		res.status(500).error(e); // error()はカスタムで実装したmiddleware
	}
};
import { BaseError } from "make-error";

export default class CustomHttpError extends BaseError {
  constructor(status, msg) {
    super(msg);
    this.status = status;
  }
}

※上記のコードはいわゆる Express の RequestHandler で、API の実装側で以下のように利用されるもの。

router.get('/', verifyAccess(), async (req, res) => {
	...
});

"if (!authorization)" の条件に対するテストを書いてみる

途中までは自分で書いておき、以下の状態から Copilot にサジェストをお願いしてみる。

import { describe } from 'vitest';
import verifyAccess from '../../srv/lib/verify-access';

const reqHander = verifyAccess();

describe('test for verify-access', () => {
	// please write your test here by vitest for reqHander
	test('authorization header missing', async () => { // <- ここでTabキー
});

すると、以下のようにテストコードを生成してくれた。そのまま流すとエラーになるが、テストしたい内容に合わせて修正すると、期待通りにテストする手伝いをしてくれる上、テスト自体も All Green になった。

動画を見ると分かるが、ほとんどコードを書かずとも Copilot の方でサジェストしてくれたコードでテストが書けている事が分かる。AI ペアプロ、すごいですね~。

※上記の動画では next の関数の実装が Error を throw する実装になっており、テストコードらしくないので vitest のtoHaveBeenCalled()で修正した。この修正も Copilot とやってみたが、以下の動画の通りかなり簡単に修正できた。

"if (type !== 'Bearer')" の条件に対するテストを書いてみる

もう 1 つテスト("if (type !== 'Bearer')" の分岐を確認するテストを追加してみる)。

最初のテストに比べ、まとめてサジェストされなくなったが、それでも改行して Tab キーでどんどんテストを書く事ができた。そしてこのテストではテストの期待値の確認の部分は全く修正が不要で、最初からテストしたい内容をテストするコードが生成されていた。

まとめとして

Copilot を利用する事でコードを人がコードを書く必要のある部分を大きく減らす事ができ、生産性の向上につながると感じた。あくまで筆者の肌感覚ではあるが、テストパターンを増やす場合はどのif分岐をテストしたいのか?をコメントで書くだけでほぼ修正不要なテストコードが生成されたことを考えると、今回の検証で生産性としては30~40%は向上したのではないかと思う。

また、ユニットテストなどのテストコードの実装は何かとしんどさを感じる事もあるが、Copilot を利用してテストコードを書いてもらう事で、テストコードを書く時間・心理的な壁も大きく減るので、どんどん活用していきたいと思った。

ただ、テストコードに限った話ではないが、コードの意味が分からないと保守・改修はできないので、書かれているコードを理解し読めるように勉強は引き続き必要だと思った。特にテストコードにおいては、テストしたい事がテストできているのか分からないといくらテストを書いても意味ないので、vitest でテストを書くならそれの書き方や使える機能への理解は必須だろう。

※Copilotの利用にあたっては、Configuring GitHub Copilot settings on GitHub.comなどを参考に、公開コードに一致するサジェストを許可するか?などの設定(著作権やCopilotによる情報収集の許可)を確認してから利用する必要があるだろう。

おまけ

モックを利用したテストコードの生成をやってみる

上記で取り上げていたコードは vitest のモックを利用した実装になっていなかった。Copilot にモックを利用した実装を、という指示を行ってテストを実装してみた。

動画で実装したコードは以下。

import { describe, expect, test, vi } from "vitest";
import CustomHttpError from "../../srv/lib/custom-http-error";
import verifyAccess from "../../srv/lib/verify-access";

const reqHander = verifyAccess();

describe("test for verify-access", () => {
  test("authorization header missing", async () => {
    const req = { headers: {} };
    const res = {
      status: vi.fn().mockReturnThis(),
      error: vi.fn(),
    };
    const next = vi.fn();
    await reqHander(req, res, next);
    expect(res.status).toHaveBeenCalledWith(500);
    expect(res.error).toHaveBeenCalledWith(
      new CustomHttpError(401, `authorization header missing`)
    );
    expect(next).not.toHaveBeenCalled();
  });
});

少しコードに関して補足をする。

  • vi.fn()
    vi.fn()を利用すると、関数が呼び出されるたびにその呼び出し引数・戻り値・インスタンスを記録してくれる。つまり、上記のコードで言えば、res.status に vi.fn()を指定しているので、res.status()が呼び出される度にその引数などを記録してくれるので、toHaveBeenCalledWithでのチェック(ある関数が特定のパラメータで少なくとも一度は呼び出されたかどうかチェック)する事ができる。

《この公式ブロガーの記事一覧》


執筆者プロフィール:Katayama Yuta
認証認可(SHIFTアカウント)や課金決済のプラットフォーム開発に従事。リードエンジニア。
経歴としては、SaaS ERPパッケージベンダーにて開発を2年経験。
SHIFTでは、GUIテストの自動化やUnitテストの実装などテスト関係の案件に従事したり、DevOpsの一環でCICD導入支援をする案件にも従事。その後現在のプラットフォーム開発に参画。

お問合せはお気軽に
https://service.shiftinc.jp/contact/

SHIFTについて(コーポレートサイト)
https://www.shiftinc.jp/

SHIFTのサービスについて(サービスサイト)
https://service.shiftinc.jp/

SHIFTの導入事例
https://service.shiftinc.jp/case/

お役立ち資料はこちら
https://service.shiftinc.jp/resources/

SHIFTの採用情報はこちら
https://recruit.shiftinc.jp/career/