LINE Messaging APIを使ってGoogleカレンダーの予定をリマインドする

以前の記事でLINE Messaging APIの始め方を説明したが、若干情報が古かったのでメモ。
今回はバックエンドサーバーはAWS SAMを使って構築してみよう。(前回はFirebaseだった)

参考資料
LINE Messaging APIのドキュメント
このドキュメントに沿って作業していく。
AWS SAM (AWS Serveless Application Model)のドキュメント
バックエンドサーバーの構築で用いる。

LINE Messaging APIの利用を開始する
LINE Messaging APIを利用するためにまずは公式アカウントを作成する必要がある。
- https://account.line.biz/signup?redirectUri=https://entry.line.biz/form/entry/unverified にアクセスしてLINE Business IDに登録する(未登録の方のみ)
- フォームに入力してLINE公式アカウントを作成する。
-
LINE Official Account Manager にログインすると、作成した公式アカウントが表示されている。
- アカウントをクリックすると以下のような管理画面が表示される。
- 「設定>Messaging API」からMessaging APIの利用を開始する
- 今度はLINE Developers Consoleにログインして、プロバイダーを選択すると、新しく追加した公式アカウントのチャネルが表示されている。
(補足)
以前はDevelopers Consoleからチャネル(Botのこと)を作成可能であったができなくなってしまった。(参考:2024年9月4日をもってLINE DevelopersコンソールからMessaging APIチャネルを直接作成することはできなくなりました | LINE Developers)

チャネルアクセストークンv2.1の発行
環境情報は以下。
- Node.js v22.11.0
- Bun: 1.1.34
前回と同様にアサーション署名キーのキーペアはブラウザのコンソールで作成した。
参考:ブラウザでキーペアを生成する - チャネルアクセストークンv2.1を発行する | LINE Developers
次に環境変数を設定するための .env
ファイルを作り、以下のように記載する。
CHANNEL_ID=xxxxx
KID=xxxxxx
-
CHANNEL_ID
:「チャネル基本設定>チャネルID」に記載されているID -
KID
:アサーション署名キーの公開鍵をDevelopers Console経由で登録した際に発行されるアクセスキー。「チャネル基本設定>アサーション署名キー」から取得できる。
そして、チャネルアクセストークンを生成するためのスクリプトmake_token.js
を以下のように用意する。
import * as jose from "jose";
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
import { dirname } from "path";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const makeJWT = async () => {
const privateKey = JSON.parse(
fs.readFileSync(path.join(__dirname, "assertion-private.key"))
);
const header = {
alg: "RS256",
typ: "JWT",
kid: process.env.KID, // チャネル基本設定>アサーション署名キー
};
const payload = {
iss: process.env.CHANNEL_ID, // チャネルID
sub: process.env.CHANNEL_ID, // チャネルID
aud: "https://api.line.me/",
exp: Math.floor(new Date().getTime() / 1000) + 60 * 25, // JWTの有効期間(UNIX時間)
token_exp: 60 * 60 * 24 * 30, // チャネルアクセストークンの有効期間
};
// JWTの生成
return new jose.SignJWT(payload).setProtectedHeader(header).sign(privateKey);
};
const createToken = async (jwt) => {
const accessTokenUrl = "https://api.line.me/oauth2/v2.1/token";
const response = await fetch(accessTokenUrl, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
grant_type: "client_credentials",
client_assertion_type:
"urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
client_assertion: jwt,
}).toString(),
});
return response;
};
(async () => {
const jwt = await makeJWT();
const accessTokenResponse = await createToken(jwt);
console.log(await accessTokenResponse.json());
})();
準備が整ったら以下のコマンドでチャネルアクセストークンを発行する。
(Bunで実行する場合)
❯ bun run scripts/make_token.js
{
access_token: 'eyJhbGxxxxxxx',
token_type: 'Bearer',
expires_in: 2592000,
key_id: 'zFHuYHxxxxxxx'
}
(Node.jsで実行する場合)
❯ node --env-file=.env scripts/make_token.js
{
access_token: 'eyJhbGxxxxxxx',
token_type: 'Bearer',
expires_in: 2592000,
key_id: 'zFHuYHxxxxxxx'
}
前回からの変更点
作業としては以上だが、JWTを生成する際に前回の方法からいくつか変更した。
(前回の方法:JWTを生成する - LINE Messaging APIを使ってオウム返しbotを作成する (Cloud Functions for Firebase 環境))
dotenv
に依存しない
変更点1:ではなく、Node.jsの実行時に--env-file
オプションを指定する
前回はdotenv
パッケージを利用して環境変数を読み込んでいた。
しかし今回はランタイム環境をBunに変更したため、自動的に環境変数の読み込みが可能になった。そのため、dotenv
は除外することができる。
よって、以下のように実行すれば良い
bun run scripts/make_token.js
もしくは、Node.js環境においても--env-file
オプションをつければ環境変数を読み込むことができる(Node.js v20.6.0以上の場合)。
参考:Node.js の進化に伴い不要となったかもしれないパッケージたち
以下のように実行する。
node --env-file=.env scripts/make_token.js
node-jose
からjose
に変更
変更点2:JWT生成パッケージを前回はドキュメントに従ってnode-jose
を利用してJWTを生成していた。
今回はESModule形式のスクリプトファイルに変更したが、node-jose
はESModuleに対応していなかった。
そこで、ESModuleに対応しているjose
に変更した。
JWT生成のインターフェースも変更された。
new jose.SignJWT(payload).setProtectedHeader(header).sign(privateKey);
スクリプトファイルをESModuleに変更した理由は、パッケージマネージャーをBunに変更しbun init
をした際にtype: module
が指定されていたため。

AWS SAM の構築
今回はバックエンドサーバーにAWS Lambdaを用いることにする。
AWS Lambdaの構築には、AWS SAM (AWS Serveless Application Model) で構築する。

SAMの事前準備
SAMを使い始めるためにいくつかの設定が必要である。
IAM Identity Center管理下のユーザー
SAM CLIを使ってAWSのリソースをデプロイするためには、IAMのクレデンシャルが必要になる。
AWSが推奨している方法はIAM Identity CenterというIAMを管理できるサービスである。
従来方法であるIAMユーザーが発行したクレデンシャルを用いる方法は非推奨となっている。アクセスキーなどのクレデンシャルが長期的に有効となってしまうからである。
公式ドキュメントや下記のブログを参考にしてIAM Identity Center経由でログインするユーザーを作成する。
AWS CLIの設定
AWS CLIはすでにインストールしていたが、アップデートした。
aws --version
aws-cli/2.22.26 Python/3.12.6 Darwin/23.6.0 exe/x86_64
下記のドキュメントの通りにCLIを利用できるように設定する。
AWS SAM CLIの設定
下記に従ってAWS SAM CLIをインストールする。
sam --version
SAM CLI, version 1.132.0

AWS SAMアプリケーションの作成
アプリケーションの初期化
sam init
コマンドを実行して、ダイアログに沿ってAWS SAMアプリケーションを作成する。
sam init
Which template source would you like to use?
1 - AWS Quick Start Templates
2 - Custom Template Location
Choice: 1
Choose an AWS Quick Start application template
1 - Hello World Example
2 - Data processing
3 - Hello World Example with Powertools for AWS Lambda
4 - Multi-step workflow
5 - Scheduled task
6 - Standalone function
7 - Serverless API
8 - Infrastructure event management
9 - Lambda Response Streaming
10 - GraphQLApi Hello World Example
11 - Full Stack
12 - Lambda EFS example
13 - Serverless Connector Hello World Example
14 - Multi-step workflow with Connectors
15 - DynamoDB Example
16 - Machine Learning
Template: 5
Which runtime would you like to use?
1 - dotnet8
2 - dotnet6
3 - nodejs22.x
4 - nodejs20.x
5 - nodejs18.x
Runtime: 3
Based on your selections, the only Package type available is Zip.
We will proceed to selecting the Package type as Zip.
Based on your selections, the only dependency manager available is npm.
We will proceed copying the template using npm.
Would you like to enable X-Ray tracing on the function(s) in your application? [y/N]: N
Would you like to enable monitoring using CloudWatch Application Insights?
For more info, please view https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch-application-insights.html [y/N]: y
AppInsights monitoring may incur additional cost. View https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/appinsights-what-is.html#appinsights-pricing for more details
Would you like to set Structured Logging in JSON format on your Lambda functions? [y/N]: y
Structured Logging in JSON format might incur an additional cost. View https://docs.aws.amazon.com/lambda/latest/dg/monitoring-cloudwatchlogs.html#monitoring-cloudwatchlogs-pricing for more details
Project name [sam-app]: schedule-line-reminder
Cloning from https://github.com/aws/aws-sam-cli-app-templates (process may take a moment)
-----------------------
Generating application:
-----------------------
Name: schedule-line-reminder
Runtime: nodejs22.x
Architectures: x86_64
Dependency Manager: npm
Application Template: quick-start-cloudwatch-events
Output Directory: .
Configuration file: schedule-line-reminder/samconfig.toml
Next steps can be found in the README file at schedule-line-reminder/README.md
Commands you can use next
=========================
[*] Create pipeline: cd schedule-line-reminder && sam pipeline init --bootstrap
[*] Validate SAM template: cd schedule-line-reminder && sam validate
[*] Test Function in the Cloud: cd schedule-line-reminder && sam sync --stack-name {stack-name} --watch
プロジェクト直下で実行したら、schedule-line-reminder/schedule-line-reminder
のようになってしまったので、生成されたファイルをプロジェクト直下に移動した。
ビルド
下記コマンドを実行してビルドする。
sam build
.aws-sam/build
配下にビルドアーティファクトが生成されればOK。

アプリケーションのデプロイ
ビルドが完了したら下記コマンドでアプリケーションをデプロイする。
sam deploy
コマンドではtemplate.yml
をCloudFormationの形式に変換し、AWSリソースをデプロイする。
sam deploy --guided --profile <profile-name>
-
sam deploy
:AWS SAMアプリケーションをデプロイする。 -
--guided
:各種設定値をインタラクティブに設定する。 -
--profile
:AWS CLIで設定したプロファイル。(~/.aws/config
に定義されている)
なお、途中に出てくるSave arguments to configuration file
という設定をY(es)
にすると、samconfig.toml
に設定値が反映されるため、次回からは以下のコマンドでデプロイできる。
sam deploy
下記のように設定してデプロイした。
Configuring SAM deploy
======================
Looking for config file [samconfig.toml] : Found
Reading default arguments : Success
Setting default arguments for 'sam deploy'
=========================================
Stack Name [schedule-line-reminder]:
AWS Region [ap-northeast-1]:
#Shows you resources changes to be deployed and require a 'Y' to initiate deploy
Confirm changes before deploy [Y/n]: Y
#SAM needs permission to be able to create roles to connect to the resources in your template
Allow SAM CLI IAM role creation [Y/n]: Y
#Preserves the state of previously provisioned resources when an operation fails
Disable rollback [y/N]: N
Save arguments to configuration file [Y/n]: Y
SAM configuration file [samconfig.toml]:
SAM configuration environment [default]:
Looking for resources needed for deployment:
Creating the required resources...
Successfully created!
Managed S3 bucket: aws-sam-cli-managed-default-samclisourcebucket-xxxxxxx
A different default S3 bucket can be set in samconfig.toml and auto resolution of buckets turned off by setting resolve_s3=False
Parameter "stack_name=schedule-line-reminder" in [default.deploy.parameters] is defined as a global parameter
[default.global.parameters].
This parameter will be only saved under [default.global.parameters] in
/Users/yuma.ito/repositories/personal/schedule-line-reminder/samconfig.toml.
Saved arguments to config file
Running 'sam deploy' for future deployments will use the parameters saved above.
The above parameters can be changed by modifying samconfig.toml
Learn more about samconfig.toml syntax at
https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-config.html
Uploading to schedule-line-reminder/xxxxxxxxxx 4599410 / 4599410 (100.00%)
Deploying with following values
===============================
Stack name : schedule-line-reminder
Region : ap-northeast-1
Confirm changeset : True
Disable rollback : False
Deployment s3 bucket : aws-sam-cli-managed-default-samclisourcebucket-xxxxxxx
Capabilities : ["CAPABILITY_IAM"]
Parameter overrides : {}
Signing Profiles : {}
Initiating deployment
=====================
Uploading to schedule-line-reminder/xxxxxxxxxx.template 1412 / 1412 (100.00%)
Waiting for changeset to be created..
CloudFormation stack changeset
-----------------------------------------------------------------------------------------------------------------
Operation LogicalResourceId ResourceType Replacement
-----------------------------------------------------------------------------------------------------------------
+ Add ApplicationInsightsMonitor AWS::ApplicationInsights:: N/A
ing Application
+ Add ApplicationResourceGroup AWS::ResourceGroups::Group N/A
+ Add ScheduledEventLoggerCloudW AWS::Lambda::Permission N/A
atchEventPermission
+ Add ScheduledEventLoggerCloudW AWS::Events::Rule N/A
atchEvent
+ Add ScheduledEventLoggerRole AWS::IAM::Role N/A
+ Add ScheduledEventLogger AWS::Lambda::Function N/A
-----------------------------------------------------------------------------------------------------------------
Changeset created successfully. arn:aws:cloudformation:ap-northeast-1:xxxxxxxxx:changeSet/samcli-deployxxxxxxx/a9324f2b-xxxxxxxxxxxx
Previewing CloudFormation changeset before deployment
======================================================
Deploy this changeset? [y/N]: y
2025-01-01 19:40:10 - Waiting for stack create/update to complete
CloudFormation events from stack operations (refresh every 5.0 seconds)
-----------------------------------------------------------------------------------------------------------------
ResourceStatus ResourceType LogicalResourceId ResourceStatusReason
-----------------------------------------------------------------------------------------------------------------
CREATE_IN_PROGRESS AWS::CloudFormation::Stack schedule-line-reminder User Initiated
CREATE_IN_PROGRESS AWS::IAM::Role ScheduledEventLoggerRole -
CREATE_IN_PROGRESS AWS::ResourceGroups::Group ApplicationResourceGroup -
CREATE_IN_PROGRESS AWS::ResourceGroups::Group ApplicationResourceGroup Resource creation
Initiated
CREATE_IN_PROGRESS AWS::IAM::Role ScheduledEventLoggerRole Resource creation
Initiated
CREATE_COMPLETE AWS::ResourceGroups::Group ApplicationResourceGroup -
CREATE_IN_PROGRESS AWS::ApplicationInsights:: ApplicationInsightsMonitor -
Application ing
CREATE_IN_PROGRESS AWS::ApplicationInsights:: ApplicationInsightsMonitor Resource creation
Application ing Initiated
CREATE_COMPLETE AWS::IAM::Role ScheduledEventLoggerRole -
CREATE_IN_PROGRESS AWS::Lambda::Function ScheduledEventLogger -
CREATE_IN_PROGRESS AWS::Lambda::Function ScheduledEventLogger Resource creation
Initiated
CREATE_IN_PROGRESS - AWS::Lambda::Function ScheduledEventLogger Eventual consistency check
CONFIGURATION_COMPLETE initiated
CREATE_IN_PROGRESS AWS::Events::Rule ScheduledEventLoggerCloudW -
atchEvent
CREATE_IN_PROGRESS AWS::Events::Rule ScheduledEventLoggerCloudW Resource creation
atchEvent Initiated
CREATE_IN_PROGRESS - AWS::ApplicationInsights:: ApplicationInsightsMonitor Eventual consistency check
CONFIGURATION_COMPLETE Application ing initiated
CREATE_COMPLETE AWS::Lambda::Function ScheduledEventLogger -
CREATE_COMPLETE AWS::ApplicationInsights:: ApplicationInsightsMonitor -
Application ing
CREATE_IN_PROGRESS - AWS::Events::Rule ScheduledEventLoggerCloudW Eventual consistency check
CONFIGURATION_COMPLETE atchEvent initiated
CREATE_IN_PROGRESS AWS::Lambda::Permission ScheduledEventLoggerCloudW -
atchEventPermission
CREATE_IN_PROGRESS AWS::Lambda::Permission ScheduledEventLoggerCloudW Resource creation
atchEventPermission Initiated
CREATE_COMPLETE AWS::Lambda::Permission ScheduledEventLoggerCloudW -
atchEventPermission
CREATE_COMPLETE AWS::Events::Rule ScheduledEventLoggerCloudW -
atchEvent
CREATE_COMPLETE AWS::CloudFormation::Stack schedule-line-reminder -
-----------------------------------------------------------------------------------------------------------------
Successfully created/updated stack - schedule-line-reminder in ap-northeast-1

トラブルシューティング
トークンの有効期限切れ
sam
コマンド実行時に以下のエラーになった。
Error: Failed to complete an API call to fetch stack information for schedule-line-reminder: Error when retrieving token from sso: Token has expired and refresh failed
その場合、SSOで再認証すれば良い。
aws sso login --profile <profile-name>

Lambda関数でパラメータストアを利用する
Google APIのクライアントシークレット情報は秘匿情報のため安全にで管理したい。
そのため、秘匿情報はAWS SSMのパラメータストアに保存し、Lambda関数でパラメータストアから秘匿情報を取得できるようにする。

Lambda関数でTypeScriptを利用する
AWS SAMではパッケージ化する際にesbuild
を使ってTypeScriptファイルのトランスパイルをサポートしている。
template.yml
ファイルにMetadata
オブジェクトを追加する。

Google Calendar API
Googleカレンダーの情報を取得するためにはGoogle Calendar APIを利用する。

APIの認証
Google Calendar APIを利用する際にOAuth 2.0によってAPIの利用を許可する。
OAuth 2.0の利用設定
- Google Cloudプロジェクトの作成
- APIを利用するためにはGoogle Cloudプロジェクトを作成する必要がある。
- https://developers.google.com/workspace/guides/create-project?hl=ja
- Google Workspace API の有効化
- https://developers.google.com/workspace/guides/enable-apis?hl=ja
- Google Calendar APIを有効化する
- 「OAuth同意画面」に遷移する
- 同意画面が変更される表示がされていたので、リンク先に遷移する
- Google Auth Platformの画面に遷移した
- 「開始」をクリックして画面に従って設定
- アプリ名:Schedule LINE Reminder
- ユーザーの種類:外部
- テストユーザー:自分のメールアドレス
- ユーザーサポートメール:自分のメールアドレス
- 「データアクセス」画面からスコープを設定する
- Google Calendar APIのスコープ一覧:https://developers.google.com/calendar/api/auth?hl=ja
- 今回は以下のスコープを選択
-
https://www.googleapis.com/auth/calendar.calendarlist.readonly
- 登録している Google カレンダーの一覧の参照
-
https://www.googleapis.com/auth/calendar.events.readonly
- すべてのカレンダーの予定を表示
-
https://www.googleapis.com/auth/calendar.calendarlist.readonly
- 「クライアント」画面からOAuth 2.0クライアントを作成
- https://developers.google.com/workspace/guides/create-credentials?hl=ja#oauth-client-id
- アプリケーションの種類:ウェブアプリケーション
- 承認済みのリダイレクト URI:http://localhost:3000/oauth2callback
- 生成したOAuth 2.0クライアントのクライアントシークレット(JSONファイル)をダウンロード
- 絶対にGitリポジトリにコミットしないこと!

ライブラリの選定
Google Calendar APIを利用するためのNode.js用認証ライブラリはいくつか公開されている模様。
-
googleapis/nodejs-local-auth
- デモ用に作られたライブラリであり、本番環境での想定はされていない。
-
google-auth-library-nodejs
に依存している - ささっと試すには良さそう。
-
googleapis/google-auth-library-nodejs
- Google APIの認証のためのライブラリ
-
googleapis/google-api-nodejs-client
- Google APIを利用するためのライブラリ
-
google-auth-library-nodejs
に依存している
今回はGoogle Calendar APIを呼び出すため、googleapis/google-api-nodejs-client
を利用する。
インストール
Google Calendar API用のライブラリのみ追加する。
bun install @googleapis/calendar
Google API ClientのREADME (OAuth2 client の節) やsamples/oauth2.jsを見ながらOAuthでアクセス許可するスクリプトを書く。

アクセストークンとリフレッシュトークンの取得
作成したスクリプトを実行する。
bun run scripts/google-authenticate.ts
実行すると認可用のURLがコンソールに出力されるのでブラウザでアクセスする。
Authorize this app by visiting this URL: https://accounts.google.com/o/oauth2/v2/auth?access_type=offline&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcalendar.calendarlist.readonly&include_granted_scopes=true&response_type=code&client_id=xxxxxxxxx.apps.googleusercontent.com&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Foauth2callback
まだ開発中のアプリケーションなので下記のような警告が表示される。
「続行」をクリックすると、アクセス許可の画面が表示される。
許可すると以下のようにサーバーからのレスポンスが表示される。
コンソールにはアクセストークンとリフレッシュトークンが出力される。
{
access_token: "xxxxxxxxx",
refresh_token: "xxxxxxxxx",
}
これらのアクセストークンとリフレッシュトークンを使うことでGoogle APIにアクセスすることができる。

Dynamic require of "child_process" is not supported
エラー
Lambdaにデプロイして実行してみると以下のエラーになった。
{
"errorType": "Error",
"errorMessage": "Dynamic require of \"child_process\" is not supported",
"trace": [
"Error: Dynamic require of \"child_process\" is not supported",
" at file:///var/task/calendar-events-handler.mjs:11:9",
" at node_modules/google-auth-library/build/src/auth/googleauth.js (/private/var/folders/65/dbkvg0pd4kvc6jsv_fb8cmw80000gp/T/tmpixckb84_/node_modules/google-auth-library/build/src/auth/googleauth.js:29:25)",
" at __require2 (file:///var/task/calendar-events-handler.mjs:17:50)",
" at node_modules/google-auth-library/build/src/index.js (/private/var/folders/65/dbkvg0pd4kvc6jsv_fb8cmw80000gp/T/tmpixckb84_/node_modules/google-auth-library/build/src/index.js:17:22)",
" at __require2 (file:///var/task/calendar-events-handler.mjs:17:50)",
" at node_modules/googleapis-common/build/src/index.js (/private/var/folders/65/dbkvg0pd4kvc6jsv_fb8cmw80000gp/T/tmpixckb84_/node_modules/googleapis-common/build/src/index.js:16:29)",
" at __require2 (file:///var/task/calendar-events-handler.mjs:17:50)",
" at node_modules/@googleapis/calendar/build/index.js (/private/var/folders/65/dbkvg0pd4kvc6jsv_fb8cmw80000gp/T/tmpixckb84_/node_modules/@googleapis/calendar/index.ts:16:1)",
" at __require2 (file:///var/task/calendar-events-handler.mjs:17:50)",
" at <anonymous> (/private/var/folders/65/dbkvg0pd4kvc6jsv_fb8cmw80000gp/T/tmpixckb84_/src/google-calendar-api-adapter.ts:1:39)"
]
}

原因
どうやらCommonJSのパッケージとESModuleのパッケージが混ざっているとesbuildでうまくトランスパイルできないらしい。
解決方法
以下のようにBanner
というプロパティを追加して、ESModuleに対応できるようにしたらうまくいった。
Metadata:
BuildMethod: esbuild
BuildProperties:
Format: esm
Minify: false
OutExtension:
- .js=.mjs
Target: "es2020"
Sourcemap: true
EntryPoints:
- src/handlers/calendar-events-handler.ts
+ Banner:
+ - js=import { createRequire } from 'module'; const require = createRequire(import.meta.url);
なお、Banner
というプロパティはドキュメントには記載されていない。

mock.module
の設定が他のテストに影響する
Bunでは mock.module
によって他のモジュールをモックすることができる。
しかし、この設定を行うと他のテストファイルの実行に影響してしまった。
例えば、a.ts
からb.ts
をimportしていたとする。
b.ts
では文字列"b"
を返却する関数 b
をexportしており、a.ts
ではb
関数を呼び出して値を返却する関数a
をexportしている。
export const b = () => {
return "b";
};
import { b } from "./b";
export const a = () => {
return b();
};
ここで2種類のテストファイルを用意する。
モックの設定はせず、b
関数の値が返却されるか確認する a1.test.ts
ファイル
import { describe, it, expect } from "bun:test";
import { a } from "./a";
describe("a1", () => {
it("should return b()", () => {
expect(a()).toBe("b");
});
});
mock.module
を使って、b.ts
からexportされる関数をモック化して値が返却されるか確認するテストファイル
import { describe, it, expect, mock } from "bun:test";
import { a } from "./a";
describe("a2", () => {
it("should return b()", () => {
mock.module("./b", () => {
return {
b: () => "dummy",
};
});
expect(a()).toBe("dummy");
});
});
ここで、テストを走らせると失敗してしまう。
$ bun test
bun test v1.1.34 (5e5e7c60)
a2.test.ts:
✓ a2 > should return b() [0.03ms]
a1.test.ts:
1 | import { describe, it, expect, mock } from "bun:test";
2 | import { a } from "./a";
3 |
4 | describe("a1", () => {
5 | it("should return b()", () => {
6 | expect(a()).toBe("b");
^
error: expect(received).toBe(expected)
Expected: "b"
Received: "dummy"
at /Users/***/schedule-line-reminder/a1.test.ts:6:17
✗ a1 > should return b() [0.33ms]
1 pass
1 fail
2 expect() calls
Ran 2 tests across 2 files. [12.00ms]
a1.test.ts
の実行で"b"
を期待しているのに"dummy"
が返却されてしまっている。
この原因は、先に a2.test.ts
が実行されmock.module
が実行される(=dummy
が返却される関数になる)と、後続のテストa1.test.ts
にもその設定が引き継がれてしまうからである。
ちなみに、個別に実行するとテストは通る。
$ bun test a1.test.ts
bun test v1.1.34 (5e5e7c60)
a1.test.ts:
✓ a1 > should return b() [0.55ms]
1 pass
0 fail
1 expect() calls

この問題は下記のissueにも挙げられている。
このissueでは以下の方法が紹介されていた。
- テスト実行ごとにmoduleの設定をもとに戻せる
ModuleMocker
というクラスを定義して使う方法 - ファイルごとにテストを実行する方法
-
find . -name \"*.test.ts\" | xargs -I {} sh -c 'bun test {} || exit 255'
- https://github.com/oven-sh/bun/issues/7823#issuecomment-2339998968
-

自分が行った解決方法は、依存している関数(クラス)を外部から注入することでmock.module
を利用しないように変更した。
↓コミット
先程の例で説明すると、a.ts
を以下のように書き換える。
export const a = (func: () => string) => {
return func();
};
import { describe, it, expect } from "bun:test";
import { a } from "./a_new";
describe("a", () => {
it("should return func()", () => {
const dummyFunction = () => "dummy";
const result = a(dummyFunction);
expect(result).toBe("dummy");
});
});

テストが書きづらいときは設計を見直した方が良いですね。

サンプルコードは以下にアーカイブ