1. はじめに
皆様お疲れ様です。「iOS Advent Calendar」の20日目を担当させて頂きます、fumiyasac(Fumiya Sakai)と申します。何卒よろしくお願い致します。
今回はUIまわりのトピックではなく、実務や個人開発を通じてユニットテストを書く機会があり、その中で私自身も「あ、この部分は書いておいて良かった or 助けになった」と感じた例がありましたので、自分なりの事例に関してご紹介ができればと思います。
既にテストコードを実務や個人アプリ等で書かれている方にとっては、もしかしたら退屈な内容になるかも知れませんが、何がしかのご参考になれば嬉しく思います。
2. 個人開発でユニットテストを導入した際の事例紹介
私は個人的にカレンダーの年月日から該当日が祝祭日or振替休日かを判定するライブラリを開発していますが、このライブラリを開発した際に、初めてユニットテストを書きました。このライブラリは下記の様な形で利用し自前で作成したカレンダーないしは他のカレンダーUIライブラリを活用する際のいずれにも使用できることを想定して作成しました。
ライブラリ(CalculateCalendarLogic):
使い方:
/* ViewController.swift */
// Step1: ライブラリのインポート(手動で導入した場合は不要)
import CalculateCalendarLogic
// Step2: CalculateCalendarLogicのインスタンスを作成
let holiday = CalculateCalendarLogic()
...
// Step3: 使用する際は引数を入れての判定を行う
let result: Bool = holiday.judgeJapaneseHoliday(year: 2016, month: 1, day: 1)
// 実行結果
print("2016年1月1日:\(result)")
//コンソールでは 「2016年1月1日:true」 と表示されます
実際にロジック検証をしやすくすると共に、目視だと確認がしずらい部分を中心にテストを手厚くすることでロジックの誤りを発見しやすくするようにしています。
このライブラリを作成した際に得られた自分なりの知見に関する部分の紹介につきましては、下記の資料でもまとめていますので、併せてご参考にして頂ければ幸いです。
(参考スライド)自分のライブラリを1年運用をして見た振り返りと知見:
以前に書いた記事:
★2-1: 祝祭日判定で見落としやすい部分
祝祭日及び振替休日の判定ロジックを作成した際に確認・考慮漏れが起こりやすい部分が下記の4つでした。
- シルバーウィークに関する判定部分のテスト
- ゴールデンウィークに関する判定部分のテスト
- 春分の日・秋分の日に関する判定部分のテスト
- ハッピーマンデー法が適用された祝祭日に関する判定部分のテスト
ここではシルバーウィークになる条件に関して紹介すると、__「9月に国民の祝日が重なって最大5連休になる期間」のことを指し、また日本の現在の法律では「祝祭日と祝祭日の間に挟まれた平日は国民の休日となる」__という前提を踏まえた上でのロジックは下記の様になります。
※ 9月20日(敬老の日)・9月22日(秋分の日)であった場合は、9月21日は「国民の休日」となります。
例. シルバーウィークの判定ロジック部分を抜粋したもの:
public struct CalculateCalendarLogic {
/**
*
* 祝日になる日を判定する
* (引数) year: Int, month: Int, day: Int, weekdayIndex: Int
* weekdayIndexはWeekdayのenumに該当する値(0...6)が入る
* ※1. カレンダーロジックの参考:http://p-ho.net/index.php?page=2s2
* ※2. 書き方(タプル)の参考:http://blog.kitoko552.com/entry/2015/06/17/213553
* ※3. [Swift] 関数における引数/戻り値とタプルの関係:http://dev.classmethod.jp/smartphone/swift-function-tupsle/
*
*/
public func judgeJapaneseHoliday(year: Int, month: Int, day: Int) -> Bool {
・・・(省略)・・・
//(1).9月15日(1966年から2002年まで)、(2).9月の第3月曜日(2003年から): 敬老の日
case (1966...2002, 9, 15, _):
return true
case (year, 9, 15...21, .mon) where year > 2002:
return true
//9月16日: 敬老の日の振替休日
case (1973...2002, 9, 16, .mon):
return true
//9月22日 or 23日: 秋分の日(計算値によって算出)
case (year, 9, day, _)
where PublicHolidaysLawYear <= year && day == SpringAutumn.autumn.calcDay(year: year):
return true
//秋分の日の次が月曜日: 振替休日
case (year, 9, day, .mon)
where year >= AlternateHolidaysLawYear && day == SpringAutumn.autumn.calcDay(year: year) + 1:
return true
//シルバーウィークの振替休日である(※現行の法律改正から変わらないと仮定した場合2009年から発生する)
//See also: https://ja.wikipedia.org/wiki/シルバーウィーク
case (_, 9, _, _)
where oldPeopleDay(year: year) < day
&& day < SpringAutumn.autumn.calcDay(year: year)
&& getAlterHolidaySliverWeek(year: year) && year > 2008:
return true
・・・(省略)・・・
}
}
/**
*
* シルバーウィークの振替休日を判定する
* 敬老の日の2日後が秋分の日ならば間に挟まれた期間は国民の休日とする
*
*/
private func getAlterHolidaySliverWeek(year: Int) -> Bool {
return oldPeopleDay(year: year) + 2 == SpringAutumn.autumn.calcDay(year: year)
}
/**
* 指定した年の敬老の日を調べる
*/
internal func oldPeopleDay(year: Int) -> Int {
let cal = Calendar.current as NSCalendar
func dateFromDay(day: Int) -> NSDate? {
return cal.date(era: AD, year: year, month: 9, day: day, hour: 0, minute: 0, second: 0, nanosecond: 0) as NSDate?
}
func weekdayAndDayFromDate(date: NSDate) -> (weekday: Int, day: Int) {
return (
weekday: cal.component(.weekday, from: date as Date),
day: cal.component(.day, from: date as Date)
)
}
let monday = 2
return (15...21)
.map(dateFromDay)
.flatMap{ $0 }
.map(weekdayAndDayFromDate)
.filter{ $0.weekday == monday }
.first!
.day
}
}
このようにシルバーウィークになる条件の場合は、敬老の日や秋分の日の条件も入ってくるので、なかなか目視のみで確認するのは厳しい部分になるかと思います。
シルバーウィーク以外にも祝祭日or振替休日の判定に関して目視での確認が厳しそうな部分は下記の部分になるかと思います。
目視で確認するのがつらそうなケース例:
| 祝祭日 | 目視での確認が厳しいと感じる点 |
|:-----------|:------------|:------------|
|春分&秋分の日 |計算式からの算出と年ごとの日付の判定
(春分:3/20 or 3/21, 秋分:9/22 or 9/23) |
|ハッピーマンデー法 |N月の第M週の考慮と施行前後の判定
(2000年施行:成人の日&体育の日, 2003年施行:海の日&敬老の日) |
|ゴールデンウィーク |5/3〜5/5のいずれかが日曜日の場合は5/6は振替休日となる判定 |
カレンダーでの見た目:
このように複数の条件や法律の施行タイミングなどの兼ね合いも考慮しなければいけないケースがあったので、まずは2000年以降のテストケースや目視での判定が難しい部分を中心にユニットテストを書くことにしました。
★2-2: 祝祭日判定で見落としやすい部分のテストを中心に書く
次に上記で列挙した中で、シルバーウィーク・ゴールデンウィーク・春分&秋分の日のテストに関して解説をします。
基本的にはjudgeJapaneseHoliday(year: year, month: month, day: day)の引数に設定する年月日
とメソッド実行後のBoolの結果
を1つにしたタプルを作って、テストした値をあらかじめまとめた上でユニットテストを実行する形式としています。
シルバーウィーク及びゴールデンウィークに関しては、条件に合致するテストケースと併せて「紛らわしいけど条件に合致しない年度のテストケース」があるとより精密なテストになるかと思います。
1. シルバーウィークの判定に関するユニットテスト:
import XCTest
@testable import CalculateCalendarLogic
class CalculateCalendarLogicTests: XCTestCase {
・・・(省略)・・・
/**
*
* シルバーウィークの判定のテスト
* 該当テストケース1: 2009年
* 該当テストケース2: 2015年
* 該当テストケース3: 2016年
* 該当テストケース4: 2026年
* 該当テストケース5: 2032年
*
*/
func testSilverWeek() {
let test = CalculateCalendarLogic()
// テストの目的:9月の第2週の土曜日〜第3週の木曜日までで紛らわしい年のケース検出と検証
let testCases: [(Int,Int,Int,Bool)] = [
// 2009年
(2009, 9, 19, false), //土曜日
(2009, 9, 20, false), //日曜日
(2009, 9, 21, true), //月曜日(敬老の日で祝祭日)
(2009, 9, 22, true), //火曜日(国民の休日で祝祭日)
(2009, 9, 23, true), //水曜日(秋分の日で祝祭日)
(2009, 9, 24, false), //木曜日
// 2015年
(2015, 9, 19, false), //土曜日
(2015, 9, 20, false), //日曜日
(2015, 9, 21, true), //月曜日(敬老の日で祝祭日)
(2015, 9, 22, true), //火曜日(国民の休日で祝祭日)
(2015, 9, 23, true), //水曜日(秋分の日で祝祭日)
(2015, 9, 24, false), //木曜日
// 2016年 ※シルバーウィークにならないが一見わかりにくいケース(最初に実装した際に勘違いしていた)
(2016, 9, 19, true), //月曜日(敬老の日で祝祭日)
(2016, 9, 20, false), //火曜日
(2016, 9, 21, false), //水曜日
(2016, 9, 22, true), //木曜日(秋分の日で祝祭日)
(2016, 9, 23, false), //金曜日
(2016, 9, 24, false), //土曜日
// 2026年
(2026, 9, 19, false), //土曜日
(2026, 9, 20, false), //日曜日
(2026, 9, 21, true), //月曜日(敬老の日で祝祭日)
(2026, 9, 22, true), //火曜日(国民の休日で祝祭日)
(2026, 9, 23, true), //水曜日(秋分の日で祝祭日)
(2026, 9, 24, false), //木曜日
// 2032年
(2032, 9, 18, false), //土曜日
(2032, 9, 19, false), //日曜日
(2032, 9, 20, true), //月曜日(敬老の日で祝祭日)
(2032, 9, 21, true), //火曜日(国民の休日で祝祭日)
(2032, 9, 22, true), //水曜日(秋分の日で祝祭日)
(2032, 9, 23, false), //木曜日
]
testCases.forEach { (arg) in
let (year, month, day, expected) = arg
let result = test.judgeJapaneseHoliday(year: year, month: month, day: day)
guard let weekday = Weekday(year: year, month: month, day: day) else { XCTFail() ; return }
let message = "\(year)年\(month)月\(day)日(\(weekday.longName)):\(result)"
// judgeJapaneseHolidayメソッドの戻り値がBoolなので期待した結果に合致するかを判定する
if expected {
XCTAssertTrue (result, message)
} else {
XCTAssertFalse(result, message)
}
}
}
・・・(省略)・・・
}
2. ゴールデンウィークの判定に関するユニットテスト:
import XCTest
@testable import CalculateCalendarLogic
class CalculateCalendarLogicTests: XCTestCase {
・・・(省略)・・・
/**
*
* ゴールデンウィークの判定のテスト
*
*/
func testGoldenWeek() {
let test = CalculateCalendarLogic()
// テストの目的:ゴールデンウィークの振替休日の判定が正しいことをチェックする
let testCases: [(Int,Int,Int,Bool)] =
[
// 2017年
(2017, 5, 2, false), //火曜日
(2017, 5, 3, true ), //水曜日(祝日)
(2017, 5, 4, true ), //木曜日(祝日)
(2017, 5, 5, true ), //金曜日(祝日)
(2017, 5, 6, false), //土曜日
// 2019年
(2019, 5, 2, false), //木曜日
(2019, 5, 3, true ), //金曜日(祝日)
(2019, 5, 4, true ), //土曜日(祝日)
(2019, 5, 5, true ), //日曜日(祝日)
(2019, 5, 6, true ), //月曜日(振替休日)
// 2020年
(2020, 5, 2, false), //土曜日
(2020, 5, 3, true ), //日曜日(祝日)
(2020, 5, 4, true ), //月曜日(祝日)
(2020, 5, 5, true ), //火曜日(祝日)
(2020, 5, 6, true ), //水曜日(振替休日)
(2020, 5, 7, false), //木曜日
// 2021年
(2021, 5, 2, false), //日曜日
(2021, 5, 3, true ), //月曜日(祝日)
(2021, 5, 4, true ), //火曜日(祝日)
(2021, 5, 5, true ), //水曜日(祝日)
(2021, 5, 6, false) //木曜日
]
testCases.forEach { (arg) in
let (year, month, day, expected) = arg
let result = test.judgeJapaneseHoliday(year: year, month: month, day: day)
guard let weekday = Weekday(year: year, month: month, day: day) else { XCTFail() ; return }
let message = "\(year)年\(month)月\(day)日(\(weekday.longName)):\(result)"
// judgeJapaneseHolidayメソッドの戻り値がBoolなので期待した結果に合致するかを判定する
if expected {
XCTAssertTrue (result, message)
} else {
XCTAssertFalse(result, message)
}
}
}
・・・(省略)・・・
}
3. 春分・秋分の日の判定に関するユニットテスト:
春分・秋分の日についても、カレンダーを見ただけではわかりにくい部分なので、こちらも春分の日・秋分の日のリストを元にして該当日がtrue
となる様なユニットテストを下記のような形で書いていきます。
(欲を言うならば、春分の日・秋分の日の振替休日に関するテストも一緒にやっておいた方がより良かったかもしれませんね。)
import XCTest
@testable import CalculateCalendarLogic
class CalculateCalendarLogicTests: XCTestCase {
・・・(省略)・・・
/**
*
* 春分の日・秋分の日の組み合わせが正しいかのテスト
* 計算式算出の参考:http://koyomi8.com/reki_doc/doc_0330.htm
* テストケース参考:http://www.nao.ac.jp/faq/a0301.html
*
*/
func testShunbunAndShubun() {
let test = CalculateCalendarLogic()
// テストの目的:設定した日付が祝祭日となっていることをチェックする
let testCases: [(Int,Int,Int,Bool)] = [
// 2000年
(2000, 3, 20, true),
(2000, 9, 23, true),
・・・(※2000年〜2030年までのテストケースを準備する)・・・
// 2030年
(2030, 3, 20, true),
(2030, 9, 23, true)
]
testCases.forEach { (arg) in
let (year, month, day, expected) = arg
let result = test.judgeJapaneseHoliday(year: year, month: month, day: day)
guard let weekday = Weekday(year: year, month: month, day: day) else { XCTFail() ; return }
let message = "\(year)年\(month)月\(day)日(\(weekday.longName)):\(result)"
// judgeJapaneseHolidayメソッドの戻り値がBoolなので期待した結果に合致するかを判定する
if expected {
XCTAssertTrue (result, message)
} else {
XCTAssertFalse(result, message)
}
}
}
・・・(省略)・・・
}
作りたての際には、実機確認の目視だけで行なっていたので、実装漏れや考慮漏れが発生したり等が割とあったのですが、ユニットテストを導入することで2000年以降の祝祭日ないしは振替休日の判定の確認が正しいかを容易に行うことが可能になったのは非常に大きな収穫でした。
ライブラリのソース自体は1つのStructファイルで依存するライブラリもないので、現状はシンプルですが、付随する機能を追加する場合にがあった際は祝祭日ないしは振替休日の判定部分が正しいことが前提となるので今後もしっかりと運用をしていきたいと思います。
3. 実務でユニットテストを導入した際の事例紹介
実務で開発したアプリの中でも、既存機能の中でここは人力でテストをするのは実はしんどいであろうと思われる部分やリニューアル時に新しく開発した部分についてテストコードを記載した事例を紹介できればと思います。
下記のような形での役割分担で進めており、自分は新規開発部分とは別にUI部分のことをメインにやっていたので、同僚の方が書いた機能の部分については、仕様の理解や把握をしておきたいという意図も込めてユニットテストを書いていくことにしました。
開発メンバー構成と役割:
登場人物 | 担当部分 |
---|---|
私 | ・リニューアルに伴うUIに関する変更&調整 ・同僚のコードのレビュー |
同僚 | ・新しい機能の開発 ・デザイナーやPOとの仕様や機能に関する調整全般 ・私のコードのレビュー |
アプリの機能に関する前提事項:
- アプリ内の表示データはAPI経由ではなく、予めRealm内に保持しておく
- Realmのスキーマ構造については
テーブル名Entity.swift
のような形のファイルがある - Realm内のデータ取得についてはModelクラス相当のものを用意しておき、例えば
テーブル名.getXXX()
みたいにしてデータを取得する
★3-1: 新規に機能を開発した部分が正しく動作するかを判定するテスト
実務で開発したアプリの中で、食材ごとのリスト一覧ページを新規機能として追加しました。その際に主だった機能としては、
- カテゴリーで表示するデータの絞り込みを行う
- 食材に紐づくチェックリストとメモを登録できる
がありますが、ここでは「カテゴリーで表示するデータの絞り込みを行う」機能に関するテストについて考えていきます。
図解:
前提条件や機能の概要に関しては、まとめると下記のような形になります。
食材ごとのリスト一覧ページに表示するデータが正しく取得できているかの判定に関するユニットテスト例:
下記のテストコードで行なっていることとしては、表示するためのデータを取得するためのメソッドであるIngredient.Ingredient.getArraysForIngredientListByCategory(category: カテゴリ定義したenumが入る)
が正しい動作をするか否かの確認になります。
その2: カテゴリー選択がカテゴリーAの場合
のテストケースに関しては、「取得した食材データに紐づくカテゴリーがカテゴリーAのものと一致する」状態を正しい状態としています。
import XCTest
@testable import #project-name#
import RealmSwift
/**
* 食材ごとのリスト画面のテーブル表示用に使用するメソッド:
*
* Ingredient.getArraysForIngredientListByCategory(category: カテゴリ定義したenumが入る)
* に関するテストケース
*
* case1. すべて
* case2. カテゴリーA
*
* の2ケースに対してユニットテストを行っている。
*
* ※ 引数の指定がない場合は全部の食材内容が表示されるので異常系の処理に関しては考慮していない
*/
class IngredientTests: XCTestCase {
//カテゴリー選択:カテゴリーA
let selectedCategory: IngredientListCategoryEnum = IngredientListCategoryEnum.categoryA
//食材IDが7のものを取得する(カテゴリーAに含まれている)
let categoryAEntity: IngredientEntity? = Ingredient.findById(7)
//カテゴリーAのIDは11である
let categoryACategory1Id = 11
override func setUp() {
super.setUp()
}
override func tearDown() {
super.tearDown()
}
func testGetArraysForIngredientListByCategory() {
//その1: 引数なしの場合
let ingredientListByCategoryAboutAll = Ingredient.getArraysForIngredientListByCategory()
XCTAssertNotEqual(0, ingredientListByCategoryAboutAll.0.count, "セル表示用オブジェクトの配列が0件でないこと")
XCTAssertNotEqual(0, ingredientListByCategoryAboutAll.1.count, "食材リストのセクション表示の配列が0件でないこと")
XCTAssertNotNil(ingredientListByCategoryAboutAll.0.first?.first?.ingredient?.name, "セル表示用オブジェクトから取得した食材名がnilでないこと")
XCTAssertNotNil(ingredientListByCategoryAboutAll.1.first?.id, "食材リストのセクション表示のカテゴリIDが取得できること")
//その2: カテゴリー選択がカテゴリーAの場合
let ingredientListByCategoryA = Ingredient.getArraysForIngredientListByCategory(category: selectedCategory)
XCTAssertNotEqual(0, ingredientListByCategoryA.0.count, "セル表示用オブジェクトの配列が0件でないこと")
XCTAssertNotEqual(0, ingredientListByCategoryA.1.count, "食材リストのセクション表示の配列が0件でないこと")
//カテゴリーAの食材データのみを抽出
var ingredientEntitiesOfCategoryA: [IngredientEntity] = []
for (_, ingredientCellObjectList) in ingredientListByCategoryA.0.enumerated() {
let ingredientCellObjects = ingredientCellObjectList.map({ $0.ingredient })
for (_, targetCategoryAEntity) in ingredientCellObjects.enumerated() {
ingredientEntitiesOfCategoryA.append(targetCategoryAEntity!)
}
}
XCTAssertTrue(ingredientEntitiesOfCategoryA.contains(categoryAEntity!), "カテゴリーIDがカテゴリーAの食材が含まれていること")
let ingredientCategory1CategoryAName = ingredientEntitiesOfCategoryA.1.map({ $0.id }).first
XCTAssertEqual(CategoryACategory1Id, ingredientCategory1CategoryAName, "取得できたカテゴリーIDがカテゴリーAのものと一致すること")
}
}
★3-2: 選択した検索条件のデータが取得できているかを判定するテスト
このアプリでは、既存の機能として検索条件から該当する条件に合致するレシピデータの一覧を表示する機能がありました。この部分に関してもアプリのメインとなる機能であったことやデータが増えてくると人力での確認が難しくなる部分でしたので、この部分にも数ケース程ユニットテストを追加することにしました。
検索処理の概要:
前提条件や機能の概要に関しては、まとめると下記のような形になります。
条件に該当するレシピデータに関しては、Searcher.getRecipes()
を使用し、引数に絞り込み条件をそれぞれセットするような形になっています。
また引数の定義は下記のような形になっています。
引数名 | 型名 | 概要 |
---|---|---|
periodType | PeriodTypeEnum型 | A or B or C or Allから1つ |
searchWord | String型 | 検索に含みたい文字列 |
notSearchWord | String型 | 検索に含みたくない文字列 |
limitationFlg | Bool型 | 限定条件の有無 |
excludeFlgs | Stringの配列型 | 検索から外したい項目名の文字列が入る配列 |
図解:
選択した検索条件のデータが取得できているかの判定に関するユニットテスト例:
import XCTest
@testable import #project-name#
import RealmSwift
class SearcherTests: XCTestCase {
override func setUp() {
super.setUp()
}
override func tearDown() {
super.tearDown()
}
func testGetRecipes() {
//その1: 完全一致するレシピが存在する場合はレシピデータの先頭にくる
let searchResultsOne = Searcher.getRecipes(
PeriodTypeEnum.all,
searchWord: "XXX",
notSearchWord: "",
limitationFlg: false,
excludeFlgs: []
)
XCTAssertEqual(
Recipe.findById(100), //レシピID:100 → レシピ名:XXX
searchResultsOne.first,
"検索に含めるワード「XXX」で完全一致検索を行った際には、「XXX」のデータが先頭にくること"
)
//その2: 完全一致する語句が存在する場合
let searchResultsTwo = Searcher.getRecipes(
PeriodTypeEnum.all,
searchWord: "バナナミルク",
notSearchWord: "",
limitationFlg: false,
excludeFlgs: ["milk"] //検索から外したい項目で「牛乳」を選んでいる
)
XCTAssertNotEqual(
Recipe.findById(511), //レシピID:511 → レシピ名:バナナミルク
searchResultsTwo.first,
"検索から外したい項目で「牛乳」を選び検索に含めるワード「バナナミルク」で完全一致検索を行った際には、バナナミルクのデータが先頭にこないこと"
)
//その3: 除外する食材のチェックボックスを有効にした食材を使用している検索ワードで検索する場合
let searchResultsThree = Searcher.getRecipes(
PeriodTypeEnum.latter,
searchWord: "納豆",
notSearchWord: "",
limitationFlg: false,
excludeFlgs: ["soy"] //検索から外したい項目で「大豆」を選んでいる
)
XCTAssertEqual(
[],
searchResultsThree,
"検索から外したい項目で「大豆」を選び検索に含めるワードに「納豆」を設定した際には、1件もデータを取得できないこと"
)
}
}
上記のテストに関しては、登録されているデータがまだまだ少ないうちや機能がシンプルな場合はさほど気にならない部分なのかもしれませんが、将来的な機能開発に向けての既存仕様の把握や登録されているデータの誤りがないかのチェックの意味も含めて手始めに、
- アプリの中でメインの部分になる機能
- これまでになかった新しい機能
の部分から優先的かつ少しずつユニットテストを追加していく形から始めると、良いかなと個人的に感じました。
4. あとがき
自分のライブラリで導入したユニットテストに関しても、テストケースとしてはまだまだ甘い部分があったり、現状は2000年以降の考慮が中心であったりするのでまだまだ改善点や改良点は多くあるので、今後も継続的に改善を加えていく予定です。
比較的シンプルなライブラリのテストケースを書いてみる所から初めてみると、実務でもなるべく__「テストが後からでも導入できる様な設計や構成にしよう」という意識が少しずつできてきたことや、「該当データ取得や追加・削除などのビジネスロジックに近しい部分からまずは徐々にテストコードを追加していく」__という取り組みが実践できたので、とても良い足がかりにすることが幸いにもできました。
特に前者のテストがしやすい、ないしは後から追加しやすい構成にすることの重要性やどのように設計を改めて見直すかのポイントに関しては、@fromkkさんの資料がとてもわかりやすかったです。
若干短めの記事になってしまい恐縮ではございますが、UIの実装サンプルを作成する際や実務の中でもテストしやすい設計や書き方を心がけていきたいと改めて襟を正す次第ですm(_ _)m