Node.js で Lambda ハンドラのテストを書くときに AWS イベントを生成する
Sunday, December 18, 2022 04:04:00 PM
AWS Lambda ハンドラをテストしたいとき、多くはリクエストのバリデーションの結果で 400 エラーを返すのか、ロジックが正常終了したとき 200 を戻すのか、みたいなことを書きたいことがあると思います。
TypeScript で Lambda ハンドラを記述する場合は、以下のようになると思います。
import { APIGatewayProxyHandler } from 'aws-lambda';
export const hello: APIGatewayProxyHandler = async (event) => {
// バリデーションやロジックの呼び出し
}
このとき event
は APIGatewayProxyEvent
型になるのですが、項目がたくさんあります。
自分で定義した interface などの場合、 factory.ts だったり、factory-bot みたいなライブラリを使って書けば良いのですが、 Lambda ハンドラのイベントはとても項目が多いので、 factory 定義を書くのも大変です。
faker-ts の利用
そこで以前は faker-ts というライブラリを使っていて @types/aws-lambda
から適当な値を生成していました。
import { tsMock } from 'faker-ts';
const mocker = tsMock(['/node_modules/@types/aws-lambda/index.d.ts']);
const event = mocker.generateMock('APIGatewayProxyEvent');
しばらくこの方法で問題はなかったのですが、 Node.js v16 と jest の組み合わせになり、メモリ不足エラーが出るようになってしまったので、見直しが必要になりました。
faker-ts の仕組み
faker-ts は以下の2つのステップから構成されていました。
- typescript-json-schema で TypeScript コードからJSONスキーマの生成
- json-schema-faker でJSONスキーマからJSONオブジェクトの生成
これを別々に実行して試していくプランとしました。
typescript-json-schema の利用
まずはJSONスキーマの生成から。 このNPMパッケージには CLI ツールも付いているので、npm script でスキーマファイルを生成できるようにしました。
"scripts": {
"tjs": "typescript-json-schema --required"
}
のように package.json に記述したら、コマンドを実行して JSONスキーマファイルを生成します。
$ npm run tjs -- -o test/schema/APIGatewayProxyEvent.json node_modules/@types/aws-lambda/index.d.ts APIGatewayProxyEvent
このようにして、必要な型のJSONスキーマを test/schema
の下に生成していきます。
json-schema-faker の利用
テストコードでは、以下のようにすることでJSONオブジェクトを生成します。
import { JSONSchemaFaker } from 'json-schema-faker';
import { faker } from '@faker-js/faker/locale/ja';
JSONSchemaFaker.option('useExamplesValue', true);
JSONSchemaFaker.option('useDefaultValue', true);
JSONSchemaFaker.option('faker', () => faker);
const event = <APIGatewayProxyEvent>JSONSchemaFaker.generate(require('test/schema/APIGatewayProxyEvent.json'));
event.body = /** テストで使う入力値 */
さいごに
これで TypeScript Interface から型定義に従った Fake オブジェクトが作れるようになりました。
faker-ts
はあまりメンテされていなかったので、コードを fork してライブラリを最新追従したりして使っていたのですが、その手間も不要となりました。
2つに処理を分割してわかったのは、 typescript-json-schema
が結構時間がかかっていたので、これを CLI にすることでメモリの利用状況も少なくなりました。
そのため、毎回JSONスキーマを生成しなくなった分、テストも速くなり良いことが多かったです。
この方法だと自分で作った TypeScript の型などでも一度 JSONスキーマを生成しておけば、複数の factory ライブラリを使わなくても良くなるので、良いかもしれません。(ただ現時点では factory-bot とかの方が使い勝手が良いとは思います。たとえば buildList みたいなことができないので)
Node.js で AWS SQS を使ったコードの自動テストを記述する
Sunday, December 18, 2022 02:53:00 PM
前回のNode.js で BigQuery を使ったコードの自動テストを記述すると同じように外部サービスを使った部分の自動テストについてです。 今回は AWS SQS を使ったコードの自動テストを書く場合です。
BigQuery と違い、 AWS SQS のエミュレータは検索するとすぐに見つかりました。 たとえば roribio16/alpine-sqs というもの。 こちらは日本語の使ってみた記事もいくつか出ています。
roribio16/alpine-sqs
さっそく上記の記事や、公式ドキュメントを参考に動かしてみます。
Dockerコンテナをあげて aws cli から実行してみると、簡単に利用することができたので、 Node.js + Jest のテストコードを書いて試してみます。 すると ECONNRESET エラーとなって動きません。おやぁ?
GitHub のリポジトリを見てみると、関連する issue が出てました。 どうも Apple Silicon (いわゆるM1とかM2とか)からだとエラーになるようです。 解決するための Pull Request も出てるんですが、マージされる気配がないようです。 現在、開発用の端末は M1 MacBook Pro を使っているので、困ってしまいました。
alpine-sqs の Dockerfile を見ると elasticmq-server
を使っているようなので、それを調べてみます。
elasticmq-server
ElasticMQ の GitHub を見てみると、普通に
Amazon SQS-compatible interface
って書いてありました。 これだけで良いのでは?という感じです。 (alpine-sqs の利点は何だったんだろう?)
で、それっぽいキーワードで検索すると、記事もありました。
- Amazon SQS互換のElasticMQを使って、Temoporary Queue+RPCを試してみる
- Amazon SQSをDockerを使ってローカルで実行
- LocalでSQS(ElasticMQ)を動かしてみた
- ElasticMQでAmazon SQSのローカルテスト環境を作る
調べ方の問題ですかね?キーワードの指定の方法によっては、こっちの方がたくさん出てきました。
さてこちらも Docker コンテナがあるので、それを使って起動します。
$ docker run -d -rm -p 9324:9324 -p 9325:9325 softwaremill/elasticmq-native
起動時にキューを作っておくには config ファイルを記述してファイルをマウントします。
$ docker run -d -rm -p 9324:9324 -p 9325:9325 -v `pwd`/custom.conf:/opt/elasticmq.conf softwaremill/elasticmq-native
config の書き方は、 GitHub に例が書いてあるので、そちらを参考にしてください。
Node.js からの接続
まず aws-sdk のクレデンシャルに適当な値を設定しておきます。 あとは SQS クラスのインスタンスを生成するだけです。
import * as AWS from 'aws-sdk';
import { SQS } from 'aws-sdk';
AWS.config.credentials = {
secretAccessKey: 'secretAccessKey',
accessKeyId: 'accessKeyId',
sessionToken: 'sessionToken',
};
let sqs: SQS;
beforeAll(() => {
sqs = new SQS({
region: 'ap-northeast-1',
endpoint: 'http://localhost:9324',
})
});
キューのURLはデフォルト設定だと http://localhost:9324/000000000000/キュー名
のようになります。
このあたりは aws-cli などでキュー一覧を取得してもわかるので、そこから情報を取得しても大丈夫でしょう。
さいごに
はい。 これで AWS SQS を使ったコードの UnitTest も書けるようになりますね。 どんどん UnitTest を充実させていきましょう。
Node.js で BigQuery を使ったコードの自動テストを記述する
Sunday, December 18, 2022 11:15:00 AM
BigQuery へクエリするコードを書くとき、どうしていますか? ORM を使って RDB を使うコードを書いている場合などは、 SQLite などを使って UnitTest を書いていることもあるでしょう。 BigQuery についても、何かそういったことができないかな?と思い、調べていました。
BigQuery Emulator
そんなとき、ちょうど BigQuery Emulator の存在を知り、試してみることにしました。
作者の goccy さんのスライドです。
GitHub の README によると Docker コンテナでも動かせるようですので、私たちはそれを利用することにしました。
$ docker pull ghcr.io/goccy/bigquery-emulator:latest
コードのサンプルとしては Python と Golang での書き方は提供されていたので、 Node.js でも何とかなるんじゃない?… と思いやり始めたのです。
Node.js – BigQuery Emulator
Node.js で BigQuery を利用するには、公式のクライアントをインストールします。
$ npm i @google-cloud/bigquery
接続の方法は Python のクライアントを見る限り、
- エンドポイント
- プロジェクトID
- 匿名接続(AnonymousCredentials)
を指定すれば良さそうです。
client_options = ClientOptions(api_endpoint="http://0.0.0.0:9050")
client = bigquery.Client(
"test",
client_options=client_options,
credentials=AnonymousCredentials(),
)
Node.js だとこんな感じでしょうか?
new BigQuery({
projectId: 'test',
apiEndpoint: 'http://0.0.0.0:9050',
credentials: /* 何を指定すれば良いんだ? */
});
ここで、 credentials
の指定に AnonymousCredentials
みたいなものが無いことに気がつきます。
JavaScript 以外のクライアント、上記の Python や Go には、匿名接続のオプションがあるようです。
現時点の credentials に指定できるのは、公式ドキュメントによると
credentials?: CredentialBody | ExternalAccountClientOptions;
だけです。
ExternalAccountClientOptions
は外部のアカウント連携を使った認証をする場合のオプションになっています。
@google-cloud/bigquery のコードを調べる
クライアントの言語違いで接続が変わるわけではないので、 BigQuery のサーバー側は匿名接続できるようになっているが、クライアント側の実装がサポートしていない、ということは想定できるでしょう。 こういうときは、クライアントのソースコードを調べるしかありません。 正面突破は無理でも、何かハックできる方法があるかもしれません。
BigQuery クラス
GitHubのリポジトリGoogle BigQuery: Node.js Clientです。 まず、 BigQuery クラスのコンストラクタを調べます。
このあたりで、オプションを作り直して親クラスのコンストラクタを呼び出すみたいです。
const config = {
apiEndpoint: options.apiEndpoint!,
baseUrl,
scopes: ['https://www.googleapis.com/auth/bigquery'],
packageJson: require('../../package.json'),
autoRetry: options.autoRetry,
maxRetries: options.maxRetries,
};
if (options.scopes) {
config.scopes = config.scopes.concat(options.scopes);
}
super(config, options);
credentials
は options
の中に入ったままなので、 BigQuery 特有のオプションを config
に置き換えて親クラスである @google-cloud/common/Service
を呼び出す感じでしょうか。
Service クラス
Service
クラスは @google-cloud/common
という共有パッケージにあるので、そこのリポジトリを調べます。
Serviceクラスのコードです。
このあたりで、リクエストコンフィグを作って、ユーティリティクラスのクレデンシャルファクトリを呼び出すみたいです。
const reqCfg = extend({}, config, {
projectIdRequired: this.projectIdRequired,
projectId: this.projectId,
authClient: options.authClient,
credentials: options.credentials,
keyFile: options.keyFilename,
email: options.email,
token: options.token,
});
this.makeAuthenticatedRequest =
util.makeAuthenticatedRequestFactory(reqCfg);
this.authClient = this.makeAuthenticatedRequest.authClient;
this.getCredentials = this.makeAuthenticatedRequest.getCredentials;
なるほど、なるほど。
makeAuthenticatedRequestFactory
続いて、 makeAuthenticatedRequestFactory を見てみましょう。
コードには関数定義がいろいろあって、最終的には makeAuthenticatedRequest
という関数をファクトリメソッドは戻すようです。
ふむふむ。
const mar = makeAuthenticatedRequest as MakeAuthenticatedRequest;
mar.getCredentials = authClient.getCredentials.bind(authClient);
mar.authClient = authClient;
return mar;
つまり BigQuery
クラスのインスタンスを生成すると、認証クライアントまでは生成するけど、接続などにはいかないことがわかります。
続いてファクトリメソッドの引数を調べてみましょう。
興味深いオプションが2つありますね。
認証エンドポイントをカスタマイズできるようです。 では、ファクトリでは、このオプションをどうやって使っているのか見てみましょう。
ここにありました。
const authorizeRequest = async () => {
if (
reqConfig.customEndpoint &&
!reqConfig.useAuthWithCustomEndpoint
) {
// Using a custom API override. Do not use `google-auth-library` for
// authentication. (ex: connecting to a local Datastore server)
return reqOpts;
} else {
return authClient.authorizeRequest(reqOpts);
}
};
コメントを読んでください。
ex: connecting to a local Datastore server
「例えば、ローカルのサーバーとかに接続するときな」って、今回の用途じゃないですか!
どうやったら指定できるのか?
さて、ここまでの調査を振り返りましょう。
- BigQuery クラスで Service クラスのコンストラクタを呼び出す
- Service クラスでリクエストコンフィグを作って、ユーティリティクラスのクレデンシャルファクトリを呼び出す
BigQuery クラスからのオプションでは customEndpoint
を指定する方法がない!という結論。
ちょっと待てよ?
サービスクラスで生成したファクトリどこに入れてたっけ?
this.makeAuthenticatedRequest =
util.makeAuthenticatedRequestFactory(reqCfg);
this
(=== BigQuery クラスのインスタンス) ですね。
サービスクラスの定義を見てみましょう。
makeAuthenticatedRequest: MakeAuthenticatedRequest;
private じゃないってことは、オーバーライドできるじゃないですか。
ハックする
import { BigQuery } from '@google-cloud/bigquery';
import { util } from '@google-cloud/common';
let bigQuery: BigQuery;
beforeAll(() => {
const options = {
projectId: 'test',
apiEndpoint: 'http://0.0.0.0:9050',
baseUrl: 'http://0.0.0.0:9050',
scopes: ['https://www.googleapis.com/auth/bigquery'],
packageJson: require('@google-cloud/bigquery/package.json'),
customEndpoint: true,
};
bigQuery = new BigQuery(options);
bigQuery.makeAuthenticatedRequest = util.makeAuthenticatedRequestFactory(options);
});
options の基本項目は、 BigQuery クラスのコンストラクタで指定されていた項目をそのまま流用しています。
それに customEndpoint
を指定して makeAuthenticatedRequestFactory
を呼び出しインスタンス変数の値を上書きします。
うまくいきましたよね!
これで Node.js の UnitTest から BigQuery Emulator を使ってクエリのテストコードを書くことができるようになりました。 BigQuery Emulator があって良かった!
@swc-node/jest を使ってテストを高速化する(v1.5対応版)
Tuesday, July 12, 2022 06:23:00 PM
少し前のブログで @swc-node/jest を使ってテストを高速化する という記事を書いたのですが、最後のまとめで以下のように紹介していました。
ちなみに 1.5系からは tsconfig を読み込むように変更されているのですが、現時点私たちのプロジェクトではビルドが失敗するので、まだ 1.4 系を利用しています。 問題はすでに issue TypeScript path mapping is not working. になっており、 パスエイリアスを使っているときに、うまくファイルが import できないところなのですが、これが解決されれば transformer の設定も不要になるので、 とても便利になるはずです。
そこから他の issue を調べたりしているうちに解決策がわかったので、今回の記事で追記していきます。
@swc-node/jest
の最新版(現時点では 1.5.2
)を使います。
パスエイリアスの指定方法
1.5系からは tsconfig を読み込むので、 jest.config.js
の transform
は以下のように記述すれば良いことになっています。
/** @type {import('@jest/types').Config.InitialOptions} */
const config = {
moduleFileExtensions: ['ts', 'js'],
transform: {
'^.+\\.ts$': ['@swc-node/jest']
},
testMatch: ['**/*.test.ts'],
moduleNameMapper: {
'@/(.*)$': '<rootDir>/$1',
},
};
module.exports = config;
パスエイリアスは moduleNameMapper
に記述されているとおり、テストコードからは @/domain/entities/user-entity
みたいに @
をルートにしてパスエイリアスを使えるようにしています。
で、このままだとパスエイリアスがすべて解決されません、というのが前回までの内容でした。
今回は調べるなかで、以下のように指定することでパスエイリアスがうまく利用できるようになったので、紹介します。
/** @type {import('@jest/types').Config.InitialOptions} */
const config = {
moduleFileExtensions: ['ts', 'js'],
transform: {
'^.+\\.ts$': [
'@swc-node/jest',
// ここから
{
paths: {
'@/*': [`${__dirname}/*`],
}
}
// ここまで
]
},
testMatch: ['**/*.test.ts'],
moduleNameMapper: {
'@/(.*)$': '<rootDir>/$1',
},
};
module.exports = config;
transform
で paths
の指定をするとパスエイリアスが使えるようになります。
tsconfig にも paths
の指定が以下のようになっています
"paths": {
"@/*": ["./*"]
},
で、tsconfig に記述しているとおりコードが書き変わると、そのまま相対パスになってしまい、 インポートパスが見つからないということになっていました。
TypeScript は tsconfig でパスエイリアスを指定するとき ./*
のように書くとプロジェクトルートからの解釈をしてくれるのですが、 swc
ではそのように解釈はしてくれません。
そのため jest.config.js
の transform
で絶対パスに上書きするように記述します。 jest.config.js
はJavaScriptコードなので、 __dirname
を使って絶対パスに変更することができるようになります。
これで安心して @swc-node/jest
も最新版に追従できるようになりました。
もし同じような問題になっている人の手助けになる記事になっていれば幸いです。
Rails 7の採用提案で注目を集め始めた Import maps の過去、現在、そして未来について...
Friday, May 06, 2022 05:03:00 PM
import maps については 2019年ごろに度々当ブログにて紹介してきました。 で、それからもずっと issue なんかを追いかけてはきたのですが、昨年 Rails 7 に import maps が入るっていう驚きがあったのと、 社内で発表する機会もあったので、少しスライドにまとめて発表しました。
で、これを発表したのが 2021/11/29 (いい肉の日だった)のですが、それからすぐ 2021/12/15 Rails 7 がリリースされました。
Rails 7 における import maps
Rails 7.0: Fulfilling a vision
で、import maps に関連する部分として Importmap for Rails というライブラリができました。
Rails本体の Gemfile に入っているので Rails 7+ をインストールすると、自動的に入ります。
Railsのアプリケーションジェネレータを使うとき、 javascript
オプション( -j
)のデフォルトも importmap
のようです。
/railties/lib/rails/generators/rails/app/app_generator.rb#L26
ドキュメントにも書いてあるとおり、現時点は Chrome/Edge 89+ だけが import maps をサポートしているので、es-module-shimsを使っているようです。
ライブラリの中では importmaptagshelper.rb で import maps 関連のタグを出力できるヘルパー関数を用意しています。
ドキュメントによると、設定は config/importmap.rb
にするようです。
公式ドキュメントによると、以下のように記述するようです。
# config/importmap.rb
pin "react", to: "https://ga.jspm.io/npm:react@17.0.2/index.js"
もしくはコマンド実行すると、最新のバージョンで config/importmap.rb
に追記してくれるようです。
Using npm packages via JavaScript CDNs
./bin/importmap pin react react-dom
なるほど、といいたいところですが、ここで一つ疑問がおきます。
これって npm outdated
や npm audit
てきなことや、 dependabot での脆弱性チェックってどうなっちゃうのかしら?と。
パッケージの最新追従はどうなるのか?
さきほどの importmap
コマンドですが、 unpin
という削除するオプションはあるようですが、アップデート系は書いてないので無いのかな?
あとは import maps の json を出力する json
オプションがあるだけみたいだ(執筆時点のソースコードを確認した)。
脆弱性があったパッケージのアップデートとか気にしないのかな?気にするよね?
少し package-lock.json
から config/importmap.rb
に変換するライブラリとかないかな?と調べてみたりもしたのですが、
見つかりませんでした。
まぁそれより、ちゃんと issue 見るか、と思っていたらちゃんとありました。 /bin/importmap outdated
で、上記に対するPRもありました。 Add outdated and audit commands
こんな感じで出力されるようです。
Audit
+-------------+----------+---------------------+--------------------------------------------------------+ | Package | Severity | Vulnerable versions | Vulnerability | +-------------+----------+---------------------+--------------------------------------------------------+ | glob-parent | high | <5.1.2 | Regular expression denial of service | | is-svg | high | >=2.1.0 <4.3.0 | ReDOS in IS-SVG | | is-svg | high | >=2.1.0 <4.2.2 | Regular Expression Denial of Service (ReDoS) | | lodash | critical | <4.17.12 | Prototype Pollution in lodash | | lodash | high | <4.17.21 | Command Injection in lodash | | lodash | high | <4.17.19 | Prototype Pollution in lodash | | lodash | high | <4.17.11 | Prototype Pollution in lodash | | lodash | low | <4.17.5 | Prototype Pollution in lodash | | lodash | moderate | <4.17.21 | Regular Expression Denial of Service (ReDoS) in lodash | | lodash | moderate | <4.17.11 | Prototype pollution in lodash | | nth-check | moderate | <2.0.1 | Inefficient Regular Expression Complexity in nth-check | +-------------+----------+---------------------+--------------------------------------------------------+ 11 vulnerabilities found: 6 high, 3 moderate, 1 low, 1 critical
Outdated
+-----------------+---------------+---------------+
| Package | Current | Latest |
+-----------------+---------------+---------------+
| @jspm/core | 2.0.0-beta.18 | 2.0.0-beta.19 |
| @jspm/core | 2.0.0-beta.2 | 2.0.0-beta.19 |
| aaaasssstimulus | 2.0.0 | Not found |
| glob-parent | 3.1.0 | 6.0.2 |
| is-glob | 3.1.0 | 4.0.3 |
| is-svg | 3.0.0 | 4.3.2 |
| lodash | 4.17.1 | 4.17.21 |
| nth-check | 1.0.0 | 2.0.1 |
| react | 16.0.0 | 17.0.2 |
| stimulus | 2.0.0 | 3.0.1 |
+-----------------+---------------+---------------+
10 outdated packages found
CI で dependabot みたいなのがどう検知するかといった課題はあるかもですが、いったんコマンド結果が出力できるので、それをシェルでゴニョゴニョすれば アップデートの検知や脆弱性の発見はできるようになりそうですね。
合わせて読みたい
さいごに Zenn にも良い記事があったので、紹介しておきます。 Rails 7.0 で標準になった importmap-rails とは何なのか?
Recent Articles
- GAE gen1 で動いている PHP5.5 で作った個人開発サービスを gen2 PHP8.2 へ移行した1年記 〜 その 2 2024/03/20
- GAE gen1 で動いている PHP5.5 で作った個人開発サービスを gen2 PHP8.2 へ移行した1年記 〜 その 1 2024/03/20
- マルチプルレポをモノレポへコミットログを残しながら移行する 2023/09/27
- tsyringe を TypeScript 5 で使う方法 2023/05/02
- LocalStack を使って aws-sdk の Integration Test を実行する 2023/04/19
- AWS SDK v3 のモジュールと利用方法 2023/04/18
- ts-jest が esbuild/swc をトランスフォーマーに使って高速化していた 2023/04/13
- aws-sdk v3 を使うライブラリを作ったときは、なるべく peerDependencies に設定しよう 2023/04/11
- aws-sdk v2 が 2023 年中にメンテナンスモードになる 2023/04/06
- Node.js v18 / aws-sdk v3 の Lambda アプリが突然動かなくなる 2023/04/05