関数 sumItem
があり、2つの引数の型 Item
があるとします。
export const sumItem = (a: Item, b: Item) => a.unitPrice + b.unitPrice
Itemの型は
export type Item = { name: string, description: string, contents: string, unitPrice: number, note: string, }
のように、すべて定義が必要だったとします。
そんな中、 sumItem
関数に対してテストコードを定義したくなったけど、 Item
型を持つテストデータを用意するのが手間、みたいなことがありました。
(注) 上記例だとそこまで手間ではないですが、実際には以下な感じでした。
- 上記例では5項目程度ですが、実際にはもっとある
- 上記例では「sumItemの引数は、内部で使う属性
unitPrice
だけにする」こともできますが、実際には複数の属性を利用している- そのため、関数名が
sumItem
というような名前になっています...
- そのため、関数名が
そこで、「引数の型を元にモックオブジェクトを生成し、テストで必要な属性だけモックオブジェクトにセットする」ができないか試してみたときのメモを残します。
なお、この記事よりもより良い書き方があればおしらせください。
目次
環境
- TypeScript 5.1.3
- Jest 29.5.0
テスト対象のプロダクションコードは以下です。
なお、TypeScriptでは型を作るためにtype文とinterface宣言があることから、今回はどちらにも対応できるかを試してみます。
type文と関数の場合の実装です。
export type Item = { name: string, description: string, contents: string, unitPrice: number, note: string, } export const sumItem = (a: Item, b: Item) => a.unitPrice + b.unitPrice
interface宣言と関数の場合の実装です。
export interface Product { name: string, description: string, contents: string, unitPrice: number, note: string, } export const sumProduct = (a: Product, b: Product) => a.unitPrice + b.unitPrice
jest.createMockFromModuleでモックオブジェクトを生成し、jest.Mocked で型注釈する
型からモックオブジェクトを生成するには、Jestの createMockFromModule
が使えそうでした。
jest.createMockFromModule(moduleName) | The Jest Object · Jest
また、生成したモックオブジェクトには、 jest.Mock<T>
で型注釈を追加しておきます。
Mock Functions · Jest
type文を使っている関数のテストコードを createMockFromModule
を使って書いてみます。
test('sumItem', () => { const a: jest.Mocked<Item> = jest.createMockFromModule<Item>('./sum') a.unitPrice = 100 const b: jest.Mocked<Item> = jest.createMockFromModule<Item>('./sum') b.unitPrice = 250 expect(sumItem(a, b)).toBe(350) expect(a.name).toBe(undefined) expect(b.name).toBe(undefined) })
ちなみに、モックオブジェクトの生成と値の設定に冗長感があれば、ファクトリ関数を作っても良さそうです。
なお、Item.unitPriceと同じ型を引数として受け取れるようにするため、
Pick<Item, 'unitPrice'>
で型を抜き出して新しい型を作る- その型に対して
['unitPrice']
でunitPriceの型を抜き出す
としています。
参考:Pick<T, Keys> | TypeScript入門『サバイバルTypeScript』
// ファクトリ関数 const itemFactory = (unitPrice: Pick<Item, 'unitPrice'>['unitPrice']): jest.Mocked<Item> => { const mockObject: jest.Mocked<Item> = jest.createMockFromModule<Item>('./sum') mockObject.unitPrice = unitPrice return mockObject } // テストコード本体 test('sumItem with factory', () => { const a: jest.Mocked<Item> = itemFactory(100) const b: jest.Mocked<Item> = itemFactory(250) expect(sumItem(a, b)).toBe(350) expect(a.name).toBe(undefined) expect(b.name).toBe(undefined) })
一方、interface宣言を使った関数の場合のテストコードはこちらです。同じような実装になりました。
test('sumProduct', () => { const a: jest.Mocked<Product> = jest.createMockFromModule<Product>('./sum') a.unitPrice = 200 const b: jest.Mocked<Product> = jest.createMockFromModule<Product>('./sum') b.unitPrice = 400 expect(sumProduct(a, b)).toBe(600) expect(a.name).toBe(undefined) expect(b.name).toBe(undefined) })
jest-mock-extended を使う場合
他に方法がないか調べていたところ、Jestで使うモックをより便利にするライブラリとして jest-mock-extended
がありました。
marchaos/jest-mock-extended: Type safe mocking extensions for Jest https://www.npmjs.com/package/jest-mock-extended
そこで、jest-mock-extended
の使い心地も試してみます。
type文で使うときは以下となります。Jest単体よりも実装量が減りました。
Jest単体との違いとしては、未定義の属性にアクセスすると、 [Function mockConstructor]
が返ってくるという点です。
test('sumItem', () => { // type文の場合、初期値の設定も行える const a: MockProxy<Item> = mock<Item>({unitPrice: 100}) const b: MockProxy<Item> = mock<Item>({unitPrice: 200}) expect(sumItem(a, b)).toBe(300) // undefinedではなく、 [Function mockConstructor] が返ってくる // expect(a.name).toBe(undefined) // => Expected: undefined, Received: [Function mockConstructor] })
一方、interface宣言に対して使うときは、 mock
にモックの属性の初期値を設定しようとすると型エラーになってしまいます。
const a: MockProxy<Product> = mock<Product>({unitPrice: 100}) // => Argument type {unitPrice: number} is not assignable to parameter type import("ts-essentials").DeepPartial<Product> | undefined
そのため、以下のように属性の初期値は後で与える必要があります。なお、未定義の属性にアクセスしたときの挙動はtype文と同じです。
test('sumProduct', () => { const a: MockProxy<Product> = mock<Product>() a.unitPrice = 100 const b: MockProxy<Product> = mock<Product>() b.unitPrice = 200 expect(sumItem(a, b)).toBe(300) // undefinedではなく、 [Function mockConstructor] が返ってくる // expect(a.name).toBe(undefined) // => Expected: undefined, Received: [Function mockConstructor] })
参考
Introduce a more flexible and better typed way to mock APIs · Issue #7832 · jestjs/jest
ソースコード
Githubに上げました。
https://github.com/thinkAmi-sandbox/typescript_jest-example
今回のプルリクはこちら。
https://github.com/thinkAmi-sandbox/typescript_jest-example/pull/1