TypeScriptの型データから自動でfactoryを作る
この記事はLAPRAS Advent Calendar 2021の19日目の記事です。
What is this?
TypeScriptの型定義を元にして、ランタイムで動作するモック を作ることができるts-auto-mockというライブラリを紹介します。
Why?
TypeScriptで(あるいは他のどんな言語でも)ソフトウェア開発をしていてこんなことないでしょうか?
🤷♂️ 「テストを書くぞ」
🤷♂️ 「ここのオブジェクトはproductionで使うものを使えないから、モックしないといけないな」
🤷♂️ 「あ、参照しているこっちのオブジェクトもモックしなきゃいけないな」
🤷♂️ 「あ、参照しているこっちのオブジェクトもモックしなきゃいけないな(N回繰り返す)」
🤷♂️ 「というか外部ライブラリのこの巨大なオブジェクト全部モックするのはつらくない??🥺」
🤷♂️ 「でも型定義は全部あるんだから、型を元にしてランタイムで動くように適当にモック作れないのかな?」
これを実現します。
ts-auto-mock を簡単に紹介
たとえばこのような型定義のオブジェクトをモックしたいとします。
interface Person {
id: string;
getName(): string;
details: {
phone: number
}
}
// ジェネリクスでモックしたい型を渡す
const person = createMock<Person>()
console.log('------person------')
console.log('id', person.id);
console.log('name', person.getName());
console.log('details', person.details.phone);
これを実行すると、このようにログが出ます。
------person------
id id6d7203
name id6ca860
details -8033.633002643623
また、
createMock<Person>({
details: {
phone: 7423232323
}
});
このように記述すると、
指定したプロパティに対するモックの挙動を明示的に指定することもできます。
さて
TypeScriptの型情報はトランスパイル後に失われることは自明なので、
ここで
「え?なんでジェネリクスで渡した情報がランタイムで利用できているの??👀」
という疑問があるかと思います。
その疑問に答えるために、
ts-auto-mockをどのようにセットアップするかをまず説明します。
セットアップ方法
実は、
ts-auto-mockは、通常の tsc
でトランスパイルしても動作しません。
tsc
自体は通りますが、実行時に下記のようなエラーが出ます。
Error:
hey, it looks like ts-auto-mock is not configured correctly! You can find the instructions here https://typescript-tdd.github.io/ts-auto-mock/installation.
If you need further assistance feel free to drop a message on slack. (link at the bottom of https://typescript-tdd.github.io/ts-auto-mock)
at r.createMock (/Users/yuichkun/Documents/workspaces/ts-auto-mock-handson/node_modules/ts-auto-mock/index.js:1:502)
at Object.<anonymous> (/Users/yuichkun/Documents/workspaces/ts-auto-mock-handson/dist/app.js:18:46)
at Module._compile (internal/modules/cjs/loader.js:1085:14)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:1114:10)
at Module.load (internal/modules/cjs/loader.js:950:32)
at Function.Module._load (internal/modules/cjs/loader.js:790:12)
at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:76:12)
at internal/main/run_main_module.js:17:47
そのため、tsc
を用いない方法でトランスパイルすることで、
トランスパイル時にコードのtransformを行う ことでランタイムで動作するモックの実現をしています。
詳しいセットアップ方法については公式のガイドに譲りますが、
本記事執筆時点でサポートされているトランスパイル実行方法としては、
jest + ts-jest + ttypescript
jest + ts-jest + ts-patch
Webpack
ttypescript
ts-patch
ts-node + Mocha
などがあります。
先に示した例のコードは、ttypescript
を用いて実行した例でしたので、
そのセットアップ方法だけ簡単に解説します。
ttypescriptで ts-auto-mockを使用できるようにする
※ 標準的なTypeScriptのビルド環境が既にあるとします。
-
ttypescript
とts-auto-mock
をインストールする
npm i ttypescript ts-auto-mock -D
-
tsconfig.json
に下記を追記する。
{
"compilerOptions": {
//...,
"plugins": [
{ "transform": "ts-auto-mock/transformer" }
]
},
//...
}
- buildスクリプトを書き換える
- "build": "tsc"
+ "build": "ttsc"
これだけです 👏
Webのプロジェクトだとwebpackなどが使われることが多いかと思いますが、
そちらのセットアップ方法も同じぐらい簡単なので、導入コストとしてはかなり低いのではないかと思います。
TypeScript本家との互換性は?
ttypescript
の README.md
を読むと
Instead of tsc and tsserver, use ttsc and ttsserver wrappers. This wrappers try to use locally installed typescript first.
No version lock-ins - typescript used as peer dependency.
とあり、
プロジェクトで使われているTypeScriptを内部ではpeer dependencyとして使っているようなので、「tsc
だと動いたものが動かなくなる」というようなことが起きるリスクは比較的低いのではないかと思います。
設定など
先に掲示した例では、実は random
という ts-auto-mock
の機能を有効にしていました。
{
"compilerOptions": {
//...,
"plugins": [
{
"transform": "ts-auto-mock/transformer",
"features": ["random"]
}
]
},
//...
}
ts-auto-mock
では TypeScriptのプリミティブ型に対するデフォルトのモックの挙動は以下のようになります。
number // 0
string // ""
boolean // false
boolean[] // []
void // undefined
null // null
undefined // undefined
never // undefined
random
というフィーチャーを有効にすると、number
enum
string
boolean
に対して、動的にランダムな値を返すような挙動を取るようになります。
このランダム化の実装は、ソースコード中のこのあたりにあります。
const MIN_NUMBER: number = -10000;
const MAX_NUMBER: number = 10000;
export class Random {
public static number(): number {
return Math.random() * (MAX_NUMBER - MIN_NUMBER) + MIN_NUMBER;
}
public static enumValue(...args: Array<string | number>): string | number {
return args[Math.floor(Math.random() * args.length)] ?? 0;
}
public static string(prefix: string): string {
return prefix + Math.random().toString(20).substr(2, 6);
}
public static boolean(): boolean {
return !Math.round(Math.random());
}
}
発展: traitを実現してみる
※ ここからは、筆者がなんとなく考えているアイデアレベルの雑記です。より良いアイデアがあったら是非シェアしてください!
ts-auto-mockはオブジェクトがネストしていても再帰的にモックを実装してくれるので便利なのですが、配列の型のモックのデフォルトの挙動は空配列になってしまうので、様々なパターンのモックがほしい時に、ちょっと不便だなと思いました。
理想としては、
- 可能な限り、factoryを手書きで書く部分を減らして、自動で作りたい
- でも、柔軟にモックの挙動を差し替えられるようにしたい
これらを満たしたいと考えた時、ts-auto-mockを使いつつ、Rubyの factory_bot でいうところのtraitのような機能があればよいのではないかと思いました。
jsでtraitを実装しやすそうなfactoryライブラリを探していたところfisheryというライブラリを見つけたので(ちなみにfactory_botと同じ会社でした)、
ts-auto-mock + fisheryという構成で、trait付き半自動factoryを実現します。
実装
なるべくリアルワールドにありそうな状態で検証してみたかったので、
下記のような型定義を用意してみました。
export type Organization = {
name: string
users: User[]
location: Location
}
export type User = {
id: number
name: string
isAdmin: boolean
location: Location
organization?: Organization
phoneNumber?: string
sns: SNS[]
}
export type Location = {
lat: number
name: string
}
export type SNS = {
name: string
url: string
}
先に述べた通り、この状態で素直に Organization
を createMock
すると、
name
や location
はランダムな文字列になりますが、 users
は常に空配列になってしまいます。
そこで、このようにtrait付きでfactoryを定義してみます。
import { Factory } from "fishery";
import { createMock, createMockList } from "ts-auto-mock";
import { Organization, User } from "../types";
class OrganizationTraits extends Factory<Organization> {
withUsers(n: number) {
return this.params({
users: createMockList<User>(n),
})
}
}
export const OrganizationFactory = OrganizationTraits.define(() => createMock<Organization>());
すると使う側では、
// デフォルトの設定でfactoryがビルドされるため、usersが[]
const organization = OrganizationFactory.build();
// withUsersというtraitで明示的にusersの数を指定
const organizationWithUsers = OrganizationFactory.withUsers(3).build();
orgazation
は下図のようなオブジェクトになりますが、
organizationWithUsers
は下図のようなオブジェクトになります。
このようにfactoryを記述するメリットとしては、
- ユースケースに応じていくらでもtraitを生やせる。
- fisheryの
Factory
クラスはchainableなAPIを提供しているので、UserFactory.admin().withFacebook().buildList(3)
のようにチェーンして記述できる。 - ts-auto-mockのデフォルトのモックの挙動で構わない部分は、記述しなくてもよい
- 上書きしたいところだけtraitを書けばよい
- なにも書かなかったプロパティは適当にモックしてもらえる
などがあるかと思います。
参考までに、このpocを実装したソースコード全体をgithubに置いておきます:
まとめ・雑記
ts-auto-mockの random
フィーチャーはまあまあ便利なのですが、
もうちょっとヒューマンフレンドリーなモックの値にしたいなという気持ちもだんだん湧いてきそうな気がしました。
e.g. string
型のモックを完全にランダムな文字列にするのではなく、faker
などのライブラリを使った読みやすい実装に差し替える、など
本記事執筆時点では、このようにユーザー側でrandom
の挙動を設定することができなさそうだったので、こんど時間のある時にts-auto-mockにproposalを出してPR作ってみようと思いました。
あと、まだ試してみていないのですが、web開発をしていてモックがほしくなる場面としては、テストも勿論そうですが、storybookもあるかなと思いました。
APIやpropsなどに型さえついていれば、既にそれは資産なので、比較的ローコストでモックを導入できるのではないかと思います。
Discussion