効果的なBDDテスト
Effective BDD Testing [iOS]
iOS Test Night #11
デレック・リー @derekleerock
[ GitHub・Twitter・FB・LinkedIn ]
1
自己紹介
→ Full-Stack XP/TDD/Pair Pro
Engineer @ Pivotal Labs Tokyo
→ Swift, Objective-C, JavaScript/
React, Java/Kotlin/Spring
→ Tokyo iOS Meetup Co-Facilitator
→ Aspiring Drummer/Educator
→ Weight Training, Hiking, SUP,
Ping Pong, Spikeball
→ Meditation, Essentialism (Book),
Tim Ferriss Show Podcast
2
BDDの特徴
→ テスト名は文書で書く Test names as sentences
→ 自然言語 Natural language
→ ユーザー観点 User perspective
→ 振る舞いにフォーカス Focus on behavior
3
iOSのBDD Frameworkといえば? → QUICK
describe("some object") {
context("when some condition") {
it("behaves the way I expect") {
// 準備 Arrange // GIVEN
// 実行 Act // WHEN
// 期待 Assert // THEN
}
}
}
4
iOSのBDD Frameworkといえば? → QUICK
describe("some object") {
context("when some condition") {
it("behaves the way I expect") {
// 準備 Arrange // GIVEN
// 実行 Act // WHEN
// 期待 Assert // THEN
}
}
}
4
iOSのBDD Frameworkといえば? → QUICK
describe("some object") {
context("when some condition") {
it("behaves the way I expect") {
// 準備 Arrange // GIVEN
// 実行 Act // WHEN
// 期待 Assert // THEN
}
}
}
4
iOSのBDD Frameworkといえば? → QUICK
describe("some object") {
context("when some condition") {
it("behaves the way I expect") {
// 準備 Arrange // GIVEN
// 実行 Act // WHEN
// 期待 Assert // THEN
}
}
}
4
iOSのBDD Frameworkといえば? → QUICK
describe("some object") {
context("when some condition") {
it("behaves the way I expect") {
// 準備 Arrange // GIVEN
// 実行 Act // WHEN
// 期待 Assert // THEN
}
}
}
4
"Outside-In" BDDの
ViewControllerテストの事例
テスト駆動型開発で説明する
5
テスト:実装のクラスの関数を直接呼び出す方法
describe("ログイン") {
context("ログインボタンをタップするとき") {
it("ユーザーの認証を確認する") {
let logonVC = LogonViewController(...)
logonVC.didTapLogonButton()
expect(...)
}
}
}
6
テスト:実装のクラスの関数を直接呼び出す方法
describe("ログイン") {
context("ログインボタンをタップするとき") {
it("ユーザーの認証を確認する") {
let logonVC = LogonViewController(...)
logonVC.didTapLogonButton()
expect(...)
}
}
}
6
テスト:実装のクラスの関数を直接呼び出す方法
describe("ログイン") {
context("ログインボタンをタップするとき") {
it("ユーザーの認証を確認する") {
let logonVC = LogonViewController(...)
logonVC.didTapLogonButton()
expect(...)
}
}
}
6
テスト:実装のクラスの関数を直接呼び出す方法
describe("ログイン") {
context("ログインボタンをタップするとき") {
it("ユーザーの認証を確認する") {
let logonVC = LogonViewController(...)
logonVC.didTapLogonButton()
expect(...)
}
}
}
6
コード:関数を定義しても、
UIButtonのtargetは設定していない
class LogonViewController: UIViewController {
func didTapLogonButton(sender: UIButton) {
// ...
}
}
7
テスト:ボタンをタップのふりをする方法
describe("ログイン") {
context("ログインボタンをタップするとき") {
it("ユーザーの認証を確認する") {
let logonVC = LogonViewController(...)
logonVC.logonButton.sendActions(for: .touchUpInside)
expect(...)
}
}
}
8
コード:UIButtonのtargetを設定して、
関数をprivateに変更できるようになる
class LogonViewController: UIViewController {
let logonButton: UIButton!
init() {
super.init(nibName: nil, bundle: nil)
logonButton.addTarget(
self,
action: #selector(didTapLogonButton(sender:)),
for: .touchUpInside
)
}
↓ ↓
@objc private func didTapLogonButton(sender: UIButton) {
// ...
}
}
9
コード:UIButtonのtargetを設定して、
関数をprivateに変更できるようになる
class LogonViewController: UIViewController {
let logonButton: UIButton!
init() {
super.init(nibName: nil, bundle: nil)
logonButton.addTarget(
self,
action: #selector(didTapLogonButton(sender:)),
for: .touchUpInside
)
}
↓ ↓
@objc private func didTapLogonButton(sender: UIButton) {
// ...
}
}
9
テスト:リファクタリング
extension UIButton {
func tap() {
sendActions(for: .touchUpInside)
}
}
logonVC.logonButton.sendActions(for: .touchUpInside)
↓ ↓
logonVC.logonButton.tap()
10
テスト:リファクタリング
extension UIButton {
func tap() {
sendActions(for: .touchUpInside)
}
}
logonVC.logonButton.sendActions(for: .touchUpInside)
↓ ↓
logonVC.logonButton.tap()
10
コード:理想なのは、UIButtonもprivateにしたい
class LogonViewController: UIViewController {
↓ ↓
private let logonButton: UIButton!
...
@objc private func didTapLogonButton(sender: UIButton) {
// ...
}
}
11
テスト:リファクタリング
it("ユーザーの認証を確認する") {
let logonVC = LogonViewController(...)
logonVC.logonButton.tap()
expect(...)
}
12
テスト:リファクタリング
it("ユーザーの認証を確認する") {
let logonVC = LogonViewController(...)
logonVC.logonButton.tap()
expect(...)
}
12
テスト:リファクタリング
it("ユーザーの認証を確認する") {
let logonVC = LogonViewController(...)
for subview in logonVC.view.subviews {
}
expect(...)
}
13
テスト:リファクタリング
it("ユーザーの認証を確認する") {
let logonVC = LogonViewController(...)
for subview in logonVC.view.subviews {
if let button = subview as? UIButton {
}
}
expect(...)
}
14
テスト:リファクタリング
it("ユーザーの認証を確認する") {
let logonVC = LogonViewController(...)
for subview in logonVC.view.subviews {
if let button = subview as? UIButton {
button.tap()
break
}
}
expect(...)
}
15
テスト:ボタンのテキストのテストを改善
it("ユーザーの認証を確認する") {
let logonVC = LogonViewController(...)
for subview in logonVC.view.subviews {
if let button = subview as? UIButton {
if button.titleLabel?.text == "Logon" {
button.tap()
break
}
}
}
expect(...)
}
16
テスト:ボタンのテキストのテストを改善
it("ユーザーの認証を確認する") {
let logonVC = LogonViewController(...)
for subview in logonVC.view.subviews {
if let button = subview as? UIButton {
if button.titleLabel?.text == "Logon" {
button.tap()
break
}
}
}
expect(...)
}
16
テスト:リファクタリング
it("ユーザーの認証を確認する") {
let logonVC = LogonViewController(...)
logonVC.tapButton(withText: "Logon")
expect(...)
}
17
コード:subviewに追加する
class LogonViewController: UIViewController {
private let logonButton: UIButton!
init() {
super.init(nibName: nil, bundle: nil)
logonButton.addTarget(...)
view.addSubview(logonButton)
}
...
}
18
UIButtonDecouple Repo
github.com/derekleerock/UIButtonDecouple
19
これは面白いけど、
ちょっと大変じゃない?
20
サシンクト(Succinct)を紹介!
github.com/derekleerock/Succinct
21
サシンクトのゴール ①
→ 単体テストの速さでUIもテストできるようになる
UI tests at the speed of unit tests
→ Swiftのクラスをちゃんとキャプセルカできる
Proper encapsulation
22
サシンクトのゴール ②
→ どんなアーキテクチャを使っても構わない
Architecture agnostic (MVC, MVVM, MVPなど)
→ リファクタリングの自由度が高い
Freedom to refactor
23
サシンクトの特徴
→ UIViewControllerのUIViewを回帰でViewを検索
→ テスト駆動型開発で作ったので、
テスト方法にも参考になる
→ 改善・新機能はGitHub Issueで管理している
→ OpenSource - PRs Welcome!
24
25
ご静聴ありがとうございました!
Derek Lee
@derekleerock
github.com/derekleerock/Succinct
26

Effective BDD Testing 効果的なBDDテスト [iOS]