前書き
全ての偽物を「Mock」そう呼んでいませんか?
Testingで偽物を活用することを「モック(Mock)する」と呼ぶことが多いですが、正確には、テストに偽物を使う技術を総称して「テストダブル」と呼びます。
ここでの「ダブル」は「スタントダブル(Stunt Double)」が由来となっています。
映画やドラマに出演する俳優が危険な状況を演じなければならない時、アクションのプロではない彼らが怪我をしないように、彼らにそっくりな「スタントマン」が代わりにアクションシーンを演じます。そうすることで、まるで俳優が派手なアクションを演じているかのような作品が出来上がるわけです。
Testingにおいても同じことです。
実際の通信を行うオブジェクト(本物)をテストで使ってしまうと、外部APIを実際に呼び出してしまったりします。外部APIは基本的に自分では制御することができないため、テストをすることがそもそもできません。
そこで、Test Doubleの出番です!
Test Doubleには5種類あります
- Spy(呼ばれたこと、渡した引数が正しいことを確認するダブル)
- Stub(返り値を固定するダブル)
- Dummy(無関係の依存であることを明確にするダブル)
- Mock(自己検証機能を持つSpy。Spyのリファクタリングの副産物)
- Fake(本物に近い、簡素な実装を持つダブル。実装を含んだStub)
全てをまとめて「Mock」と呼んでしまうと
「このユニットテストにおいて、この依存は何のためにあるのか?」
が、ものすごく抽象的になってしまいます。
そのため、しっかりと名前と役割を分けて、読み手がテストの意図を掴みやすくするためにも、テストダブルは必要なのです
今回は、意外と使い所が掴みにくい、Fake についての記事です
記事めちゃ長くなっちゃいましたすんません
Fakeとは?
テストにおいて「あたかも本物かのように振る舞うオブジェクト」のことです。
返り値を固定するだけのStubと比べると、Fakeは簡単な実装を含みます。
言い換えると、
Stubにロジックが含まれたら、それは Fake と呼ぶ。
ということになります。
そして、「Fakeはロジックを含む」ということは
「Fake自体のテストが必要」ということをお忘れなく。
Fakeの使い所
私的な Fake の使いどころを紹介します
- Stubではテストがうまく書けない時
- Stubにロジックを持たせたくなった時
- 本物を使っても問題ないテストだけど、本物の実装が複雑なため、テストのアレンジが大変な時
の2つだと思っています。具体例をコードで説明していきます
登場人物
-
SchoolService
: 学校の情報(生徒、先生)を処理するロジックを持つ層 -
SchoolManager
: 学校を管理する層
SchoolService
SchoolService
は下記を定義するinterfaceです
-
setHumans(humanTexts: string[])
: 受け取った文字列が生徒か先生の情報かを判断して、students
、teachers
にそれぞれ値をセットする -
getStudents(): Student[]
: 生徒の配列を返す -
getTeachers(): Teacher[]
: 先生の配列を返す
DefaultSchoolService
が実装クラスです
export default interface SchoolService {
setHumans(humanTexts: string[]): void
getStudents(): Student[]
getTeachers(): Teacher[]
}
export class DefaultStudentService implements SchoolService {
students: Student[] = []
teachers: Teacher[] = []
setHumans(humanTexts: string[]) {
const by = (human: Human) => (text: string) => {
return text.split(' ')[0] === human
}
this.students = humanTexts
.filter(by(Human.STUDENT))
.map(texts => this.createStudent(texts))
this.teachers = humanTexts
.filter(by(Human.TEACHER))
.map(texts => this.createTeacher(texts))
}
getStudents(): Student[] {
return this.students
}
getTeachers(): Teacher[] {
return this.teachers
}
private createStudent(text: string): Student {
const splitText = text.split(' ')
return {
name: splitText[1],
age: +splitText[2],
birthday: new Date(splitText[3]),
attendanceNumber: +splitText[4],
gradeInSchool: +splitText[5],
club: splitText[6],
}
}
private createTeacher(text: string): Teacher {
const splitText = text.split(' ')
return {
name: splitText[1],
age: +splitText[2],
birthday: new Date(splitText[3]),
subject: splitText[4],
employmentPosition: splitText[5],
advisorTo: splitText[6],
}
}
}
使用するモデルの定義は下記です
// 生徒の情報を持つモデル
export interface Student {
name: string
age: number
birthday: Date
attendanceNumber: number
gradeInSchool: number
club: string
}
// 先生の情報を持つモデル
export interface Teacher {
name: string
age: number
birthday: Date
subject: string
employmentPosition: string
advisorTo: string
}
// 人の列挙型
enum Human {
STUDENT = 'student',
TEACHER = 'teacher',
}
SchoolServiceのテストコード
この層のテストは今回Fakeを使うところではありませんが、仕様を伝えるためにテストを載せます。
注目して欲しいのは studentService.setHumans
に渡す引数です。
半角スペース区切りのテキストからstudent
or teacher
を判断し、名前や年齢、誕生日(Date型)などの情報を抜き取り、オブジェクトにするという処理です
こういうテキストのハンドリングは、ロジカルじゃなくて正直めんどくさいですよね
この例は、説明の便宜上あえて複雑な仕様にしています。
ここで押さえておいて欲しいのは
setHumans
には、結構面倒臭いテキストを渡さないといけない
ということです。
SchoolManager
からこのメソッドを呼び出すテストをこの後書きますが
この面倒臭さがどう影響してくるのかを実感してもらいたいです!
まずはSchoolServiceのテストコードです
describe('DefaultStudentService', () => {
describe('setHumans', () => {
test('受け取った文字列を正しいオブジェクトに変換し、students,teachersに正しくセットする', () => {
const studentText = 'student taro 18 2006-01-01 1 3 baseball'
const teacherText = 'teacher jiro 25 1998-03-03 数学 一般 baseball'
const studentService = new DefaultStudentService()
studentService.setHumans([
studentText,
teacherText,
])
const expectedStudents: Student[] = [
{
attendanceNumber: 1,
name: 'taro',
age: 18,
birthday: new Date('2006-01-01'),
gradeInSchool: 3,
club: 'baseball',
},
]
const expectedTeachers: Teacher[] = [
{
name: 'jiro',
age: 25,
birthday: new Date('1998-03-03'),
subject: '数学',
employmentPosition: '一般',
advisorTo: 'baseball',
},
]
expect(studentService.students).toEqual(expectedStudents)
expect(studentService.teachers).toEqual(expectedTeachers)
})
})
describe('getStudents', () => {
// メンバーのstudentsを返すだけなのでここでは省略
})
describe('getTeachers', () => {
// メンバーのteachersを返すだけなのでここでは省略
})
})
SchoolManager
SchoolManager
は下記を定義するinterfaceです
-
schools
: 学校の情報を配列で保持するメンバー変数 -
createSchools
: 引数で受け取った文字列の配列を元に、学校の情報を作るメソッド
DefaultSchoolManager
が実装クラスです
先ほどの SchoolService
に依存しています
export default interface SchoolManager {
schools: School[]
createSchools(schoolSummaries: SchoolSummary[])
}
export class DefaultSchoolManager implements SchoolManager {
constructor(
private schoolService: SchoolService = new DefaultStudentService(),
) {}
schools: School[] = []
createSchools(schoolSummaries: SchoolSummary[]) {
schoolSummaries.forEach(schoolSummary => {
this.schoolService.setHumans(schoolSummary.humanTexts)
const school: School = {
name: schoolSummary.name,
students: this.schoolService.getStudents(),
teachers: this.schoolService.getTeachers(),
}
this.schools.push(school)
})
}
}
使用するモデルの定義は下記です
// 学校名、生徒の配列、先生の配列を持つモデル
export interface School {
name: string
students: Student[]
teachers: Teacher[]
}
// 学校名、人の情報を持つ文字列の配列、を持つモデル
export interface SchoolSummary {
name: string
humanTexts: string[]
}
SchoolManagerのテストコード
createSchools
メソッドのテストにて、今回のFakeの使いどころを深掘っていきます
※TestDoubleの記事ですが、TDD(テスト駆動開発) の大切さも同時に伝えていけたらなと思っています
⚫︎Test Case 1
まずはschool
のname
を正しく保存するかどうかのテストケースです
これは単純ですね、2つのSchoolSummary
を準備してcreateSchools
に渡し、
その結果schoolManager.schools
に正しいものが保存されていればいいので
このようなテストが書けます
この時のSchoolService
はこのテストにとって「関係のない依存」なので
DummySchoolService
をDI(依存注入)します。関係ないので明示的にDummyにします。
テストを回すと1つ目のアサーションで落ちます
test('受け取ったSchoolSummaryのnameを、メンバーのschoolsに正しく保存する', () => {
const schoolSummaries = [
{name: 'Hoge High School', humanTexts: []},
{name: 'Fuga High School', humanTexts: []},
]
const dummySchoolService = new DummySchoolService()
const schoolManager = new DefaultSchoolManager(dummySchoolService)
schoolManager.createSchools(schoolSummaries)
expect(schoolManager.schools).toHaveLength(2) // ❌ Fail: expected [] to have a length of 2 but got 0
expect(schoolManager.schools[0].name).toBe('Hoge High School')
expect(schoolManager.schools[1].name).toBe('Fuga High School')
})
export class DummySchoolService implements SchoolService {
setHumans(humanTexts: string[]): void {}
getStudents(): Student[] {
return []
}
getTeachers(): Teacher[] {
return []
}
}
実装も簡単ですね、今はこれだけでテストが通ります。
createSchools(schoolSummaries: SchoolSummary[]) {
schoolSummaries.forEach(schoolSummary => {
const school: School = {
name: schoolSummary.name,
students: [],
teachers: [],
}
this.schools.push(school)
})
}
// 🟢 Test passed!!
⚫︎Test Case 2
次は、受け取った humanTexts
をschoolService.setHumans()
に渡しているかのテストケースです。
このあと、school
のstudents
とteachers
に正しい値を保存するかのテストをする必要があり、そのためにはschoolService.setHumans()
を前もって実行している必要があるので、このテストを先に行います。
この時のSchoolService
はこのテストにとって
「必要な引数を渡しているか、正しく呼んでいるか」を確かめるための役割なので
SpySchoolService
をDI(依存注入)します。今回はSpyです。
テストを回すと1つ目のアサーションで落ちます。
まだsetHumans
を一度も呼んでいないからです。
test('受け取ったSchoolSummaryのhumanTextsを、schoolServiceのcreateSchoolsに正しく渡して呼んでいる', () => {
const schoolSummaries = [
{name: '', humanTexts: ['human text 1']},
{name: '', humanTexts: ['human text 2']},
]
const spySchoolService = new SpySchoolService()
const schoolManager = new DefaultSchoolManager(spySchoolService)
schoolManager.createSchools(schoolSummaries)
expect(spySchoolService.setHumans_argument_humanTexts_logs)
.toHaveLength(2) // ❌ Fail: expected [] to have a length of 2 but got 0
expect(spySchoolService.setHumans_argument_humanTexts_logs[0])
.toEqual(['human text 1'])
expect(spySchoolService.setHumans_argument_humanTexts_logs[1])
.toEqual(['human text 2'])
})
ちなみにSpyはこのように作っています。
setHumans
が呼ばれる度に、setHumans_argument_humanTexts_logs
に引数のhumansTexts
を保存するようにしています。
こうすることで、複数回呼ばれた時にも正しい引数を受け取ったかどうかをテストできます
export class SpySchoolService implements SchoolService {
setHumans_argument_humanTexts_logs: string[][] = []
setHumans(humanTexts: string[]): void {
this.setHumans_argument_humanTexts_logs.push(humanTexts)
}
getStudents(): Student[] {
return []
}
getTeachers(): Teacher[] {
return []
}
}
実装はこうなります。
this.schoolService.setHumans
に必要な引数を渡して呼ぶだけでテストが通ります
createSchools(schoolSummaries: SchoolSummary[]) {
schoolSummaries.forEach(schoolSummary => {
this.schoolService.setHumans(schoolSummary.humanTexts)
const school: School = {
name: schoolSummary.name,
students: [],
teachers: [],
}
this.schools.push(school)
})
}
// 🟢 Test passed!!
⚫︎Test Case 3
次は、単一のSchoolSummaryを受け取った場合、schools
のstudents
とteachers
に正しい値を保存するかのテストケースです。
SchoolService
のsetHumans()
が実行されると、getStudents()
とgetTeachers()
が正しい値を返す想定なので、今回はgetStudents()
とgetTeachers()
の返り値を固定して、その値をschools
に正しく保存しているか、というアプローチでテストを書きます
この時のSchoolService
はこのテストにとって
「返り値を固定する」ための役割なので
StubSchoolService
をDI(依存注入)します。今回はStubです。
test('受け取った単一のSchoolSummaryのhumanTextsを、メンバーのschoolsに正しく保存する', () => {
const schoolSummary: SchoolSummary = {name: '', humanTexts: []}
const stubSchoolService = new StubSchoolService()
stubSchoolService.getStudents_returnValue = [
{
name: 'taro',
age: 0,
birthday: new Date(),
attendanceNumber: 0,
gradeInSchool: 0,
club: ''
}
]
stubSchoolService.getTeachers_returnValue = [
{
name: 'jiro',
age: 0,
birthday: new Date(),
subject: '',
employmentPosition: '',
advisorTo: '',
}
]
const schoolManager = new DefaultSchoolManager(stubSchoolService)
schoolManager.createSchools([schoolSummary])
expect(schoolManager.schools[0].students).toHaveLength(1)
expect(schoolManager.schools[0].students[0].name).toEqual('taro')
expect(schoolManager.schools[0].teachers).toHaveLength(1)
expect(schoolManager.schools[0].teachers[0].name).toEqual('jiro')
})
Stubはこのようになっています。
*_returnValue
というパブリックプロパティを用意し、外から返り値を決めれるようにしています。
export class StubSchoolService implements SchoolService {
setHumans(humanTexts: string[]): void {
}
getStudents_returnValue: Student[] = []
getStudents(): Student[] {
return this.getStudents_returnValue
}
getTeachers_returnValue: Teacher[] = []
getTeachers(): Teacher[] {
return this.getTeachers_returnValue
}
}
さて、TDD的にいうと、このテストをパスするための最低限の実装はどうなるでしょうか?
こうなります。
このテストであれば、createSchools()
に渡されるschoolSummaries
の要素は1つなので、ベタ書きでテストを通すことができちゃいますね。
createSchools(schoolSummaries: SchoolSummary[]) {
schoolSummaries.forEach(schoolSummary => {
this.schoolService.setHumans(schoolSummary.humanTexts)
const school: School = {
name: schoolSummary.name,
students: [
{
name: 'taro',
age: 0,
birthday: new Date(),
attendanceNumber: 0,
gradeInSchool: 0,
club: ''
}
],
teachers: [
{
name: 'jiro',
age: 0,
birthday: new Date(),
subject: '',
employmentPosition: '',
advisorTo: '',
}
],
}
this.schools.push(school)
})
}
// 🟢 Test passed!!
⚫︎Test Case 4 (Stub編)
では次は、複数のSchoolSummaryを受け取った場合でも、schools
のstudents
とteachers
に正しい値を保存するかのテストケースです。
Test Case 3 と同様にStubを使ってテストを書けるでしょうか?
結論 「できるけど、分かりにくいし、やりたくない」 です。
もちろん無理やり書くことはできます
test('受け取った複数のSchoolSummaryのhumanTextsを、メンバーのschoolsに正しく保存する', () => {
const schoolSummaries: SchoolSummary[] = [
{name: '', humanTexts: []},
{name: '', humanTexts: []},
]
const stubSchoolService = new StubSchoolService()
stubSchoolService.getStudents_returnValues = [
[StudentBuilder.build({name: 'ichiro'})],
[StudentBuilder.build({name: 'jiro'})],
]
stubSchoolService.getTeachers_returnValues = [
[TeacherBuilder.build({name: 'saburo'})],
[TeacherBuilder.build({name: 'shiro'})],
]
const schoolManager = new DefaultSchoolManager(stubSchoolService)
schoolManager.createSchools(schoolSummaries)
expect(schoolManager.schools[0].students).toHaveLength(1)
expect(schoolManager.schools[0].students[0].name).toEqual('ichiro')
expect(schoolManager.schools[0].teachers).toHaveLength(1)
expect(schoolManager.schools[0].teachers[0].name).toEqual('saburo')
expect(schoolManager.schools[1].students).toHaveLength(1)
expect(schoolManager.schools[1].students[0].name).toEqual('jiro')
expect(schoolManager.schools[1].teachers).toHaveLength(1)
expect(schoolManager.schools[1].teachers[0].name).toEqual('shiro')
})
Stubをこのように改造すれば、できないことはないです。
export class StubSchoolService implements SchoolService {
setHumans(humanTexts: string[]): void {
}
getStudents_callCount = 0
getStudents_returnValues: Student[][] = []
getStudents(): Student[] {
return this.getStudents_returnValues[this.getStudents_callCount++]
}
getTeachers_callCount = 0
getTeachers_returnValues: Teacher[][] = []
getTeachers(): Teacher[] {
return this.getTeachers_returnValues[this.getTeachers_callCount++]
}
}
getStudents_returnValues
に返り値としてセットしたい値を複数配列で準備しておき、getStudents
getTeachers
の呼ばれた回数を*_callCound
で保持しておき、インデックスアクセスで返り値を決める
という荒技です
でもこれって
「Stubは返り値を固定する」のルールに反していませんか?
インデックスによって可変してたら、それはもうStubではないと私は思ってしまいます。
何より、インデックスアクセスのロジックが既に含まれているので、偽物のテストがあってもいいのでは?とも思います。
こういった理由から、このようなケースではStubは適しません。
⚫︎Test Case 4 (本物編)
複数のSchoolSummaryを受け取った場合でも、schools
のstudents
とteachers
に正しい値を保存するかのテストケースに対して、Stubは適さないので
「じゃあ、本物使っちゃう?」
という発想を一度試してみたいと思います
テストはこうなります。
test('受け取った複数のSchoolSummaryのhumanTextsを、メンバーのschoolsに正しく保存する', () => {
const schoolSummaries: SchoolSummary[] = [
{
name: '',
humanTexts: [
'student ichiro 25 1998-03-03 1 3 baseball',
'teacher jiro 25 1998-03-03 数学 一般 baseball'
]
},
{
name: '',
humanTexts: [
'student saburo 25 1998-03-03 1 3 baseball',
'teacher shiro 25 1998-03-03 数学 一般 baseball'
]
},
]
const defaultSchoolService = new DefaultStudentService()
const schoolManager = new DefaultSchoolManager(defaultSchoolService)
schoolManager.createSchools(schoolSummaries)
expect(schoolManager.schools[0].students).toHaveLength(1)
expect(schoolManager.schools[0].students[0].name).toEqual('ichiro')
expect(schoolManager.schools[0].teachers).toHaveLength(1)
expect(schoolManager.schools[0].teachers[0].name).toEqual('jiro')
expect(schoolManager.schools[1].students).toHaveLength(1)
expect(schoolManager.schools[1].students[0].name).toEqual('saburo')
expect(schoolManager.schools[1].teachers).toHaveLength(1)
expect(schoolManager.schools[1].teachers[0].name).toEqual('shiro')
})
注目すべき点は
-
'student ichiro 25 1998-03-03 1 3 baseball'
のように、本物の実装でエラーなく、期待されるstudents
とteachers
が代入されるアレンジを、テストで正確に書く必要がある -
1998-03-03
のように、Date
型に適する文字列を正確に書かなければいけない - 「
getStudents()
が返す値をschools
にセットしている」ことを確かめるだけでいいので、最低限名前の情報だけアレンジすればいいのに、日付や部活の情報も入れなければいけない
冒頭で説明しましたが、本物の実装は文字列の少々複雑な処理を行なっています。
本物を使うということは、
「本物DefaultSchoolService
の実装が上手くいくように」テストでアレンジを作らないといけないんですね。
これってすごく面倒ですよね。
SchoolManager
の単体テストなのにSchoolService
の実装のことを考えてしまっている、つまり「余計なことを考える」必要が出てくるんですよね。
これを解決するのが、Fakeというわけです。
⚫︎Test Case 4 (Fake編)
前置きが長くなりすぎましたが、、、笑
いよいよFakeを使ってテストをしていきます。
まず、FakeSchoolService
のテストを書いて、実装をしていきます。
ここでもお伝えした通り、Fakeには実装が含まれるので、Fakeに対してもテストが必要になります
describe('FakeSchoolService', () => {
describe('setHumans', () => {
test('受け取った文字列からstudentかteacherかを判断し、正しくStudentをセットする', () => {
const studentText = 'student1'
const teacherText = 'teacher1'
const fakeStudentService = new FakeSchoolService()
fakeStudentService.setHumans([
studentText,
teacherText,
])
expect(fakeStudentService.students).toHaveLength(1)
expect(fakeStudentService.students[0].name).toEqual('student1')
expect(fakeStudentService.teachers).toHaveLength(1)
expect(fakeStudentService.teachers[0].name).toEqual('teacher1')
})
})
})
export class FakeSchoolService implements SchoolService {
students: Student[]
teachers: Teacher[]
setHumans(humanTexts: string[]): void {
const by = (human: Human) => (text: string) => {
return text.includes(human)
}
this.students = humanTexts
.filter(by(Human.STUDENT))
.map(humanText => StudentBuilder.build({name: humanText}))
this.teachers = humanTexts
.filter(by(Human.TEACHER))
.map(humanText => TeacherBuilder.build({name: humanText}))
}
getStudents(): Student[] {
return this.students
}
getTeachers(): Teacher[] {
return this.teachers
}
}
StudentBuilder, TeacherBuilderの説明はこちら
デザインパターンの一つ、Builder Pattern
です。
export class StudentBuilder {
static build(overrides: Partial<Student> = {}): Student {
return {
name: '',
age: 0,
birthday: new Date(),
attendanceNumber: 0,
gradeInSchool: 0,
club: '',
...overrides,
}
}
}
export class TeacherBuilder {
static build(overrides: Partial<Teacher> = {}): Teacher {
return {
name: '',
age: 0,
birthday: new Date(),
subject: '',
employmentPosition: '',
advisorTo: '',
...overrides,
}
}
}
実装は、本物と比べてすごくシンプルです。
setHumans()
の行数自体はあまり変わりませんが、大きく違うのは
-
humanText
にstudent
の文字列が含まれるものにfilterをかけ、name
にテキストをそのまま代入して他だけのStudentオブジェクトを生成し、students
メンバーに保存する -
humanText
にteacher
の文字列が含まれるものにfilterをかけ、name
にテキストをそのまま代入して他だけのTeacherオブジェクトを生成し、teachers
メンバーに保存する
このFakeの責任は
「与えられたテキストから生徒か先生かを判断し、誰かを判断できる文字列をname
に入れて保存するだけ」です
Date
型がどうとか、数値型がどうとか、name
以外の不要な情報を考える必要がなくなりました。
では、このFakeを使って SchoolManager
のテストを書いてみましょう
test('fake: 受け取った複数のSchoolSummaryのhumanTextsを、メンバーのschoolsに正しく保存する', () => {
const schoolSummaries: SchoolSummary[] = [
SchoolSummaryBuilder.build({humanTexts: ['student1', 'teacher1']}),
SchoolSummaryBuilder.build({humanTexts: ['student2', 'teacher2']}),
]
const fakeSchoolService = new FakeSchoolService()
const schoolManager = new DefaultSchoolManager(fakeSchoolService)
schoolManager.createSchools(schoolSummaries)
expect(schoolManager.schools[0].students).toHaveLength(1)
expect(schoolManager.schools[0].students[0].name).toEqual('student1')
expect(schoolManager.schools[0].teachers).toHaveLength(1)
expect(schoolManager.schools[0].teachers[0].name).toEqual('teacher1')
expect(schoolManager.schools[1].students).toHaveLength(1)
expect(schoolManager.schools[1].students[0].name).toEqual('student2')
expect(schoolManager.schools[1].teachers).toHaveLength(1)
expect(schoolManager.schools[1].teachers[0].name).toEqual('teacher2')
})
SchoolSummaryBuilderのコードはこちら
class SchoolSummaryBuilder {
static build(overrides: Partial<SchoolSummary> = {}): SchoolSummary {
return {
name: '',
humanTexts: [],
...overrides,
}
}
}
-
humanTexts
に対して['student1', 'teacher1']
のように、FakeSchoolService.setHumans()
の簡単な実装を満たす最低限の情報を設定する(日付の文字列などは入れなくてOK!) -
FakeSchoolService
によって、生徒と先生はしっかり振り分けられるので、getStudents()
とgetTeachers()
の返り値は期待通りに決まる -
createSchools
が期待通りにschools
を作成してくれるはず
という状況を作れました
このようにテストが書けたら、SchoolManager.createSchools()
の実装をしましょう
export class DefaultSchoolManager implements SchoolManager {
constructor(
private schoolService: SchoolService = new DefaultStudentService(),
) {}
schools: School[] = []
createSchools(schoolSummaries: SchoolSummary[]) {
schoolSummaries.forEach(schoolSummary => {
this.schoolService.setHumans(schoolSummary.humanTexts)
const school: School = {
name: schoolSummary.name,
students: this.schoolService.getStudents(),
teachers: this.schoolService.getTeachers(),
}
this.schools.push(school)
})
}
}
// 🟢 Test passed!!
テストがパスし、見事実装完了しました!!
createSchools
の責任は「schoolService.getStudents()
, schoolService.getTeachers()
の返り値を使ってschools
を正しく作る」ことなので、schoolService
の実装がどうなっているかを知る必要がありません。
Fakeを使うと、役割を明確に分けることができ、ほぼテスト対象のメソッドのことだけを考えれば良くなるというわけです。
まとめ
Fakeのおさらいです
⚫︎Fakeとは
- テストにおいて「あたかも本物かのように振る舞うオブジェクト」のこと
- 返り値を固定するだけのStubと比べると、Fakeは簡単な実装を含む
- Fakeは実装を含むので、Fake自体のテストが必要になる
⚫︎Fakeを使うタイミング
- 「あれ、Stubで返り値固定しちゃうと上手いことテストできないな」と思った時
- 「Stubに実装入ってたらテスト楽だな」と思った時(こうなるとそれはもうStubではなくFakeである)
- 「通信を行うわけじゃないし、本物使ったほうがテスト楽じゃね? でも実装複雑でテストのアレンジが煩雑になるな」と思った時
さいごに
開発していて、どうしてもテストが書きづらい場面って絶対に訪れると思います。
そんな時にこの記事の記憶がフワッと蘇って
「Fakeだったらいけちゃう?」と、アイデアの1つにFakeがなってくれたら、嬉しいです。
Fake以外のダブルについての記事は、気が向いたら書きます
こんなに長い記事を読んでいただいてありがとうございました!