モックを学ぶ
jest
を使っていてモックで毎回あれ?ってなるので備忘録
関連
www.tohuandkonsome.site
前提
テスト対象のtarget.js
import getName, { getAge } from "./service";
export default (id: string) => {
const { name } = getName(id);
const { age } = getAge(id);
return { name, age };
};
export const verClass = (id: string) => {
const member = new Member(id);
const { name } = member.getName();
const { age } = member.getAge();
return { name, age };
};
モック対象のservice.js
const getName = (id: string) => {
return { name: `${id}_tarou` };
};
export default getName;
export const getAge = (id: string) => {
return { age: `${id}_21` };
};
export class Member {
id: string;
constructor(id: string) {
this.id = id;
}
getName() {
return { name: `${this.id}_tarou` };
}
getAge() {
return { age: `${this.id}_21` };
}
}
基本のテスト
import target from "../../target";
it("targetにtarouを設定して実行すると{result: 'tarou'}}が返却されること", () => {
expect(target("01")).toEqual({ name: "01_tarou", age: "01_21" });
expect(verClass("01")).toEqual({ name: "01_tarou", age: "01_21" });
});
最初のモック
import target from "../../target";
jest.mock("../../service");
it("省略");
jest.mock
でモックしたいモジュールを指定する。ここでは、パスは、sample.test.ts
からの相対パスを指定している。
jest.mock
の第 2 引数にモックしたモジュールの実装を書くことができる。
何も書かないと、undefined
を返す関数が設定される。
イメージとしてはモックされたservice.ts
はこんな感じのコードに置き換えられる。
const getName = () => {};
export const getAge = () => {};
export const Member = () => {};
export default getName;
この状態でテストを実行すると、テストが失敗することが確認できる。
モックの実装を書く
default export
だけのモックを書くだけであれば、以下のようにシンプルに書ける。
jest.mock('../../service', () => {
return (id:string) => {
return { name: `${id}_tarou` };
}
}
アロー関数なのでreturn
すればよりシンプルになるけど、ひとまずそのまま。
上記の状態だと、モックされたservice.ts
は以下のように置き換えられる。
const getName = (id: string) => {
return { name: `${id}_tarou` };
};
export const getAge = () => {};
export const Member = () => {};
export default getName;
あんまり使う局面はないかもしれないけど、default export
だけじゃなくってnamed export
もモックする場合は、__esModule
オプションの設定すれば対応できる。
jest.mock('../../service', () => {
return {
__esModule:true,
default: (id:string) => { return { name:`${id}_tarou` }},
getAge: (id:string) => { return { age: `${id}_21` }},
}
}
上記に加え、named export されているMember
クラスのモックも追加する。
jest.mock('../../service', () => {
const Member = function(this: any, id:string) {
this.id = id
}
Member.prototype = {
getName: function() {
return { name: `${this.id}_tarou` }
},
getAge: function() {
return { age: `${this.id}_21` }
}
}
return {
__esModule:true,
default: (id:string) => { return { name:`${id}_tarou` }},
getAge: (id:string) => { return { age: `${id}_21` }},
Member
}
jest.mock のハマりどころ
さきほどまでは、jest.mock
配下にMember
クラスの実装を書いていた。
jest.mock("../../service", () => {
class Member {}
return {
__esModule: true,
default: (id: string) => {
return { name: `${id}_tarou` };
},
getAge: (id: string) => {
return { age: `${id}_21` };
},
Member
};
});
jest.mock
に設定する関数が長くなるので、見通しがわるくるなるので、以下のようにMember
をjest.mock
の外に出してみる。
class Member {}
jest.mock("../../service", () => {
return {
__esModule: true,
default: (id: string) => {
return { name: `${id}_tarou` };
},
getAge: (id: string) => {
return { age: `${id}_21` };
},
Member
};
});
この場合エラーになる。
なぜならjest.mock
で設定した内容は、jest 実行時にコードの先頭箇所へと hoisting される。
具体的には以下のような実行順序になる。
jest.mock("../../service", () => {
return {
__esModule: true,
default: (id: string) => {
return { name: `${id}_tarou` };
},
getAge: (id: string) => {
return { age: `${id}_21` };
},
Member
};
});
class Member {}
※ function から始まる関数宣言であれば、気にしなくてもよさそうだけど、あんまり使わない方がよいかな。
https://qiita.com/jkr_2255/items/9f9a25987dfaa81472fa
mockfn.mockImplementation で回避する
ここまでは、jest.mock
にモックするモジュールの実装を直接書いてきたが、モック関数を使うとより便利になる。
モック関数はjest.fn
で作成できて、そのモックがどんな引数で呼ばれたのか?何回呼ばれたの?とかをアサートすることができる。
ここでは、モック関数の実装を書くことができるmockImplementation
を使うことで、jest.mock
では hosting されることでできなかったことを対応してみる。
import { Member as MockedMember} from '../../service'
class Member {}
jest.mock("../../service", () => {
return {
__esModule: true,
default: (id: string) => {
return { name: `${id}_tarou` };
},
getAge: (id: string) => {
return { age: `${id}_21` };
},
Member: jest.fn()
};
});
const mockedMember = MockedMember as jest.Mock
mockedMember.mockImplementation((id:string)=>{
return new Member(id)
})
項番 1 のモックの設定は hoisting されるので、最初に設定される。
なので項番 2 の状態ではモックされているモジュールになっている。
モックされているモジュールに対して、さらにモックの変更を加えることで、以降
別の箇所でservice
モジュールを使用している箇所でその変更が反映される。
補足
あんまりわかってないのが、jest.mock
のときはクラスを渡していて、mockImplementation
のときはインスタンスを返さないとうまくいかない点。
class Member {}
jest.mock("../../service", () => {
return {
Member: Member
};
});
mockedMember.mockImplementation((id: string) => {
return new Member(id);
});
公式を見る限り、jest.mock
のモジュールファクトリは、コンストラクタ関数を返せっていってるので、クラス定義(ただん関数)でよくって、mockImplementation
のときは、オブジェクトを返すっぽいのかな。
function Member() {
this = {}
return this
}
function() {
return Object
}
jest.fn
mockImplementation
ででてきたjest.fn
について。
以下のようにjest.fn
でラップすると、モックされた関数は、jest.fn
の機能をもつ関数として使うことができる。
jest.mock("../../service", () => {
return {
__esModule: true,
default: jest.fn((id: string) => {
return { name: `${id}_tarou` };
}),
getAge: jest.fn((id: string) => {
return { age: `${id}_21` };
}),
Member: jest.fn()
};
});
上記モックをした状態で、モックされた関数を import してテスト内でアサーションすることができる。
できることはいっぱいあるので、公式を確認する。
API 呼び出しとかは、API に渡すリクエストの値をテストしたり、とある条件のときは API を呼び出さないとかあるので、そういった場合にjest.fn
を使うと便利。
その場合、beforeEach とかで、モック関数の設定を毎回リセットすることを忘れないようにする。
import getName, { getAge, Member as MockedMember } from "../../service";
it("targetにtarouを設定して実行すると{result: 'tarou'}}が返却されること", () => {
expect(target("01")).toEqual({ name: "01_tarou", age: "01_21" });
expect(getName).toHaveBeenCalledTimes(1);
expect(getAge).toHaveBeenCalledTimes(1);
expect(verClass("01")).toEqual({ name: "01_tarou", age: "01_21" });
});
jest.spyon
についてはまた別途書くかも。