Jestでのテストの基本構造
describeが大要素。その中にit, testがあり、一つのテストになっている。
describe, it, testの第一引数にはテストの説明を記述する。
第二引数には、ラムダ式でテストを記述していくのが基本形。
// example: saveで保存した組織情報をgetで取得し、その結果と保存した組織情報が正しいか照合するテスト
import { saveOrganization } from "../db/organzation"
describe('Organizationの機能テスト', () => {
// 説明を記述。他の人が何をやっているかわかりやすいように
it('IdでOrganizationを取得', async () => {
const savedOrg = await saveOrganization('Test Organization');
const result = await getOrganizationById(savedOrg.id);
expect(result).toEqual(savedOrg);
});
});
テストの前後で実行する関数の定義
describe()内で実行する。
dbへの接続、切断、データ構造の初期化などに使用する。
// Describeの初期で実行
beforeAll();
// それぞれのit()の前で実行
beforeEach();
// それぞれのit()の後で実行
afterEach();
// Describeの終わりで実行
afterAll(});
テストをスキップする
テストが完全に実行されなくなる。
describe.skip();
it.skip();
test.skip();
// it testに()をつける方が簡単
xit();
xtest();
同じテストケースを異なる引数で繰り返す
.each()で複数の要素を挿入する。
describe.each(TABLE)(NAME, FN);
it.each(TABLE)(NAME, FN);
test.each(TABLE)(NAME, FN);
// exmaple
const multiply = (a, b) => a * b;
describe('multiply', () => {
it.each([
[1, 1, 1],
[1, 2, 2],
[2, 1, 2],
[2, 2, 4],
])('multiply(%i, %i) should return %i', (a, b, expected) => {
expect(multiply(a, b)).toBe(expected);
});
});
テストを評価する
Jestでは、expect()
関数を使ってテストの評価を行う。expect()
には、実際に実行して得られた値を引数として渡す。その後、マッチャーと呼ばれるチェーン関数を使用して、取得した値が想定の値と合っているかを確認する。
-
toBe()
引数として渡された値と、expect()
で渡された値が完全に一致するかを確認する。
例:expect(name).toBe('Test Client');
-
toEqual()
引数として渡されたオブジェクトや配列と、expect()
で渡された値が同じ構造と値を持っているかを確認する。
例:expect(result).toEqual({ id: 1, name: 'John' });
-
toContain()
引数として渡された値が、expect()
で渡された配列や文字列に含まれているかを確認する。
例:expect(result).toContain('apple');
-
toHaveProperty()
引数として渡されたプロパティ名が、expect()
で渡されたオブジェクトに存在するかを確認する。
例:expect(result).toHaveProperty('id');
-
toHaveBeenCalled()
expect()
で渡されたモック関数が、テスト中に少なくとも1回は呼び出されたかを確認する。
例:expect(mockFunction).toHaveBeenCalled();
-
toHaveBeenCalledWith()
expect()
で渡されたモック関数が、テスト中に指定された引数で呼び出されたかを確認する。
例:expect(mockFunction).toHaveBeenCalledWith(42);
また、not
修飾子を使用することで、逆の条件をチェックすることもできる。
例: expect(result).not.toBe(null);
応用的なマッチャーの紹介
expect.objectContaining()
要素の一部が合っていると、テストをPASSできる。
// mockJsonの中身
//{
// id: 10,
// name: Tanaka,
// prefecture: Gunma
//}
// 要素はidとnameしかないがパスできる
expect(mockJson).toHaveBeenCalledWith(expect.objectContaining(
{
id: 10,
name: Tanaka
}
));
expect.arrayContaining()
リストを扱う。複数要素があるときに、順番を気にすることなくマッチングできる。
expect.any()
プリンシパル型のみ判定する。
expect.any()は「ここに数字が入るが、自動で登録されるため具体的な数字がわからない」というときに便利
// agentsの中身
//[
// {
//. id: 1
// name: "Agent1",
// llmModel: "gpt-3",
// prompt: "prompt sample",
// userGroupId: 10
// },
// {
// id: 2
// name: "Agent2",
// llmModel: "gemini",
// prompt: "prompt sample",
// userGroupId: 20
// },
// ]
// 複数の要素を取得した場合でも、順番を気にすることなくマッチングが可能。
expect(agents).toEqual(expect.arrayContaining([
expect.objectContaining({
name: "Agent1",
llmModel: "gpt-3",
prompt: "prompt sample",
userGroupId: expect.any(number)
}),
expect.objectContaining({
name: "Agent2",
llmModel: "gemini",
prompt: "prompt sample",
userGroupId: expect.any(number)
]));
モック化する
モック化する目的は、テスト対象の関数や機能に依存する外部の関数やクラスの動作を制御し、テストを安定して実行できるようにするため。
モック化の手順
-
jest.mock()
を使って、モック化対象の関数やクラスを指定する// 外部モジュール jest.mock('jsonwebtoken'); // 自作関数 jest.mock('../db/repository/client', () => ({ getOneClientByEmail: jest.fn(), }));
jest.fn()でファンクションを作成。Mockオブジェクトになる。
-
mockResolvedValue()
やmockRejectedValue()
を使って、モック関数の返り値を設定するgetOneClientByEmail(自作関数)をモック化することで、mockCilentの値を返す
const mockClient = { id: '123', name: 'John Doe', email: 'test@example.com', password: await bcrypt.hash('password', 10), role: 'user', organization_id: 'org123', }; // 正常動作の場合 (getOneClientByEmail as jest.Mock).mockResolvedValue([mockClient]); // 異常動作の場合 (getOneClientByEmail as jest.Mock).mockRejectedValue(new Error('Database connection error'));
-
テスト対象の関数を実行し、モック化された関数やクラスの動作を確認する
const req = mockRequest({ email: 'test@example.com', password: 'password' }); await routClientLogin(req, mockResponse);
モック化の例
-
外部APIのモック化
jest.mock('axios'); test('fetchUserData', async () => { const mockUser = { id: 1, name: 'John Doe' }; (axios.get as jest.Mock).mockResolvedValue({ data: mockUser }); const user = await fetchUserData(1); expect(user).toEqual(mockUser); });
-
クラスメソッドのモック化
jest.mock('./UserService'); test('createUser', async () => { const mockUser = { id: 1, name: 'John Doe' }; (UserService.prototype.create as jest.Mock).mockResolvedValue(mockUser); const userController = new UserController(); const user = await userController.createUser('John Doe'); expect(user).toEqual(mockUser); });
例外処理のテスト
例外処理のテストでは、関数がエラーを投げる場合の動作を確認する。
同期関数のエラーテスト
同期関数がエラーを投げる場合、toThrow
マッチャーを使ってテストを行います。
function divide(a, b) {
if (b === 0) {
throw new Error('Division by zero');
}
return a / b;
}
it('0で除算した場合、エラーが投げられること', () => {
expect(() => divide(10, 0)).toThrow('Division by zero');
});
上記の例では、divide
関数に0を渡した場合、'Division by zero'というエラーが投げられることを確認しています。expect
の引数には、エラーを投げる関数を即時関数として渡します。
toThrow
マッチャーには、期待されるエラーメッセージを文字列または正規表現で指定することができます。
it('0で除算した場合、エラーが投げられること', () => {
expect(() => divide(10, 0)).toThrow(/division by zero/i);
});
非同期関数のエラーテスト
非同期関数をテストする際、エラーが投げられる場合は await
を使わずに rejects
を使用する。これは、await
を使用すると、エラーが発生した時点でテストが失敗してしまうため。
it('無効な組織IDでロールを作成しようとした場合、エラーが投げられること', async () => {
expect(createRole(RoleTypeEnums.Admin, 0)).rejects.toThrow(/^Error creating role.*/);
});
supertestを使ったAPIのテスト
supertestを使うことで、Express.jsで作成したAPIのテストを簡単に行うことができる。ここでは、supertestを使ってルーティングを含むAPIのテストを行う方法について説明する。
テストの基本構成
まず、supertestをインポートし、テスト対象のExpressアプリケーションをインポートする。
import request from 'supertest';
import app from '../../api/v1/app';
...
次に、describe
ブロック内でit
を使ってテストケースを定義する。
describe('GET /', () => {
it('ルート("/")にGETリクエストを送信した場合、ステータスコード200が返され、レスポンスボディに"Running API"というメッセージが含まれていること', async () => {
const res = await request(app).get('/');
expect(res.status).toEqual(200);
expect(res.body).toHaveProperty('message', 'Running API');
});
});
上記の例では、ルート('/')と'/api/v1'にGETリクエストを送信した場合のテストを行っている。request(app)
でExpressアプリケーションにリクエストを送信し、expect
を使ってレスポンスのステータスコードとボディの内容を確認している。
Expressアプリケーションの設定
テスト対象のExpressアプリケーションでは、ルーティングの設定のみを行い、listen
メソッドは呼び出さないようにする。もしくは、テストファイル内でルーティングの設定を行う。
app.get("/", (req, res) => {
res.status(200).json({ message: "Running API" });
});
app.use("/api/v1", v1Router);
export default app;
これは、Jestがテスト終了時にプロセスを自動的に終了させるため、app
がリッスンし続けているとテストが終了できなくなるためである。
認証情報を追加する
認証が必要なAPIのテストを行う場合、.set
メソッドを使ってAuthorizationヘッダーにトークンを設定する。
const res = await request(app)
.get(`${route}/clients`)
.set('Authorization', `Bearer ${token}`); // Authorization ヘッダーを設定
expect(res.statusCode).toEqual(200);
expect(res.body).toHaveProperty('message', 'Clients found');
この例では、.set('Authorization', 'Bearer ${token}')
を使って、Authorizationヘッダーにベアラートークンを設定している。これにより、認証が必要なAPIのテストを行うことができる。
参考にした記事
弊社Passinate Geniusでは一緒に働く仲間を募集しています!興味をお持ちいただける方は、ホームページまで!