Welcome!
Cenny
Davidsson
https://www.programmingbooks.dev
An Ordered and Curated Reading List for Software Craftsmanship Growth
Parakey
We are looking for team members:
iOS, Web, and Embedded developers
https://www.parakey.co/company/careers
Annihilate test smells by refactoring to patterns
A procedure leading to acceptance or
rejection
— Kent Beck, Test-Driven Development: By Example
A smell is a symptom of a problem.
— Gerard Meszaros, xUnit Test Patterns: Refactoring
Test Code
Each pattern describes a problem which
occurs over and over again in our
environment, and then describes the core
of the solution to that problem, in such a
way that you can use this solution a million
times over, without ever doing it the same
way twice
— Christopher Alexander, A Pattern Language
Refactoring (verb): to restructure software
by applying a series of refactorings without
changing its observable behavior.
— Martin Fowler, Refactoring: Improving the Design
of Existing Code
Why?
Tests can quickly become a bottleneck in
an agile development process. This may not
be immediately obvious to those who have
never experienced the difference between
simple, easily understood tests and
complex, obtuse, hard-to-maintain tests.
— Gerard Meszaros, xUnit Test Patterns: Refactoring
Test Code
User Story
Guest must use a strong password
Disable save button when password is too weak (less
than 6 characters)
Let's do some Test-Driven Development!
func test_shorter_than_length_6_password_disables_save_button() throws {
let sut = RegisterViewController.make(register: { _, _ in })
sut.tableView.frame = CGRect(x: 0, y: 0, width: 100, height: 2000)
sut.loadViewIfNeeded()
let firstName = sut.tableView.cellForRow(at: IndexPath(row: 0, section: 0)).contentView.subviews.first as? UITextField
firstName?.text = "Scrooge"
firstName?.sendActions(for: .editingChanged)
let lastName = sut.tableView.cellForRow(at: IndexPath(row: 1, section: 0)).contentView.subviews.first as? UITextField
lastName?.text = "McDuck"
lastName?.sendActions(for: .editingChanged)
let password = sut.tableView.cellForRow(at: IndexPath(row: 0, section: 2)).contentView.subviews.first as? UITextField
password?.text = "money"
password?.sendActions(for: .editingChanged)
let terms = sut.tableView.cellForRow(at: IndexPath(row: 0, section: 3)).accessoryView as? UISwitch
terms?.isOn = true
terms?.sendActions(for: .valueChanged)
let saveButton = try XCTUnwrap(sut.navigationItem.rightBarButtonItem, "Found nil instead of item")
XCTAssertFalse(saveButton.isEnabled, "Item is enabled")
}
Obscure Test
It is difficult to understand the test at a
glance.
— Gerard Meszaros, xUnit Test Patterns: Refactoring
Test Code
func test_shorter_than_length_6_password_disables_save_button() throws {
let sut = RegisterViewController.make(register: { _, _ in })
sut.tableView.frame = CGRect(x: 0, y: 0, width: 100, height: 2000)
sut.loadViewIfNeeded()
let firstName = sut.tableView.cellForRow(at: IndexPath(row: 0, section: 0)).contentView.subviews.first as? UITextField
firstName?.text = "Scrooge"
firstName?.sendActions(for: .editingChanged)
let lastName = sut.tableView.cellForRow(at: IndexPath(row: 1, section: 0)).contentView.subviews.first as? UITextField
lastName?.text = "McDuck"
lastName?.sendActions(for: .editingChanged)
let password = sut.tableView.cellForRow(at: IndexPath(row: 0, section: 2)).contentView.subviews.first as? UITextField
password?.text = "money"
password?.sendActions(for: .editingChanged)
let terms = sut.tableView.cellForRow(at: IndexPath(row: 0, section: 3)).accessoryView as? UISwitch
terms?.isOn = true
terms?.sendActions(for: .valueChanged)
let saveButton = try XCTUnwrap(sut.navigationItem.rightBarButtonItem, "Found nil instead of item")
XCTAssertFalse(saveButton.isEnabled, "Item is enabled")
}
Test Utility Method
How do we reduce Test Code Duplication?
We encapsulate the test logic we want to reuse behind a suitably named utility method.
— Gerard Meszaros, xUnit Test Patterns: Refactoring
Test Code
SUT Encapsulation Method
reason for using a Test Utility Method is to encapsulate unnecessary knowledge of the
API of the SUT. What constitutes unnecessary? Any method we call on the SUT that is
not the method being tested creates additional coupling between the test and the SUT.
— Gerard Meszaros, xUnit Test Patterns: Refactoring
Test Code
func test_shorter_than_length_6_password_disables_save_button() throws {
let sut = RegisterViewController.make(register: { _, _ in })
sut.tableView.frame = CGRect(x: 0, y: 0, width: 100, height: 2000)
sut.loadViewIfNeeded()
let firstName = sut.tableView.cellForRow(at: IndexPath(row: 0, section: 0)).contentView.subviews.first as? UITextField
firstName?.text = "Scrooge"
firstName?.sendActions(for: .editingChanged)
let lastName = sut.tableView.cellForRow(at: IndexPath(row: 1, section: 0)).contentView.subviews.first as? UITextField
lastName?.text = "McDuck"
lastName?.sendActions(for: .editingChanged)
let password = sut.tableView.cellForRow(at: IndexPath(row: 0, section: 2)).contentView.subviews.first as? UITextField
password?.text = "money"
password?.sendActions(for: .editingChanged)
let terms = sut.tableView.cellForRow(at: IndexPath(row: 0, section: 3)).accessoryView as? UISwitch
terms?.isOn = true
terms?.sendActions(for: .valueChanged)
let saveButton = try XCTUnwrap(sut.navigationItem.rightBarButtonItem, "Found nil instead of item")
XCTAssertFalse(saveButton.isEnabled, "Item is enabled")
}
extension UITableViewController {
func cell(at indexPath: IndexPath) -> UITableViewCell? {
tableView.cellForRow(at: indexPath)
}
}
func test_shorter_than_length_6_password_disables_save_button() throws {
let sut = RegisterViewController.make(register: { _, _ in })
sut.tableView.frame = CGRect(x: 0, y: 0, width: 100, height: 2000)
sut.loadViewIfNeeded()
let firstName = sut.tableView.cellForRow(at: IndexPath(row: 0, section: 0)).contentView.subviews.first as? UITextField
firstName?.text = "Scrooge"
firstName?.sendActions(for: .editingChanged)
let lastName = sut.tableView.cellForRow(at: IndexPath(row: 1, section: 0)).contentView.subviews.first as? UITextField
lastName?.text = "McDuck"
lastName?.sendActions(for: .editingChanged)
let password = sut.tableView.cellForRow(at: IndexPath(row: 0, section: 2)).contentView.subviews.first as? UITextField
password?.text = "money"
password?.sendActions(for: .editingChanged)
let terms = sut.tableView.cellForRow(at: IndexPath(row: 0, section: 3)).accessoryView as? UISwitch
terms?.isOn = true
terms?.sendActions(for: .valueChanged)
let saveButton = try XCTUnwrap(sut.navigationItem.rightBarButtonItem, "Found nil instead of item")
XCTAssertFalse(saveButton.isEnabled, "Item is enabled")
}
func test_shorter_than_length_6_password_disables_save_button() throws {
let sut = RegisterViewController.make(register: { _, _ in })
sut.tableView.frame = CGRect(x: 0, y: 0, width: 100, height: 2000)
sut.loadViewIfNeeded()
let firstName = sut.cell(at: IndexPath(row: 0, section: 0)).contentView.subviews.first as? UITextField
firstName?.text = "Scrooge"
firstName?.sendActions(for: .editingChanged)
let lastName = sut.cell(at: IndexPath(row: 1, section: 0)).contentView.subviews.first as? UITextField
lastName?.text = "McDuck"
lastName?.sendActions(for: .editingChanged)
let password = sut.cell(at: IndexPath(row: 0, section: 2)).contentView.subviews.first as? UITextField
password?.text = "money"
password?.sendActions(for: .editingChanged)
let terms = sut.cell(at: IndexPath(row: 0, section: 3)).accessoryView as? UISwitch
terms?.isOn = true
terms?.sendActions(for: .valueChanged)
let saveButton = try XCTUnwrap(sut.navigationItem.rightBarButtonItem, "Found nil instead of item")
XCTAssertFalse(saveButton.isEnabled, "Item is enabled")
}
func test_shorter_than_length_6_password_disables_save_button() throws {
let sut = RegisterViewController.make(register: { _, _ in })
sut.tableView.frame = CGRect(x: 0, y: 0, width: 100, height: 2000)
sut.loadViewIfNeeded()
let firstName = sut.cell(at: IndexPath(row: 0, section: 0)).contentView.subviews.first as? UITextField
firstName?.text = "Scrooge"
firstName?.sendActions(for: .editingChanged)
let lastName = sut.cell(at: IndexPath(row: 1, section: 0)).contentView.subviews.first as? UITextField
lastName?.text = "McDuck"
lastName?.sendActions(for: .editingChanged)
let password = sut.cell(at: IndexPath(row: 0, section: 2)).contentView.subviews.first as? UITextField
password?.text = "money"
password?.sendActions(for: .editingChanged)
let terms = sut.cell(at: IndexPath(row: 0, section: 3)).accessoryView as? UISwitch
terms?.isOn = true
terms?.sendActions(for: .valueChanged)
let saveButton = try XCTUnwrap(sut.navigationItem.rightBarButtonItem, "Found nil instead of item")
XCTAssertFalse(saveButton.isEnabled, "Item is enabled")
}
extension UITableViewCell {
var textField: UITextField? {
contentView.subviews.first as? UITextField
}
}
func test_shorter_than_length_6_password_disables_save_button() throws {
let sut = RegisterViewController.make(register: { _, _ in })
sut.tableView.frame = CGRect(x: 0, y: 0, width: 100, height: 2000)
sut.loadViewIfNeeded()
let firstName = sut.cell(at: IndexPath(row: 0, section: 0)).contentView.subviews.first as? UITextField
firstName?.text = "Scrooge"
firstName?.sendActions(for: .editingChanged)
let lastName = sut.cell(at: IndexPath(row: 1, section: 0)).contentView.subviews.first as? UITextField
lastName?.text = "McDuck"
lastName?.sendActions(for: .editingChanged)
let password = sut.cell(at: IndexPath(row: 0, section: 2)).contentView.subviews.first as? UITextField
password?.text = "money"
password?.sendActions(for: .editingChanged)
let terms = sut.cell(at: IndexPath(row: 0, section: 3)).accessoryView as? UISwitch
terms?.isOn = true
terms?.sendActions(for: .valueChanged)
let saveButton = try XCTUnwrap(sut.navigationItem.rightBarButtonItem, "Found nil instead of item")
XCTAssertFalse(saveButton.isEnabled, "Item is enabled")
}
func test_shorter_than_length_6_password_disables_save_button() throws {
let sut = RegisterViewController.make(register: { _, _ in })
sut.tableView.frame = CGRect(x: 0, y: 0, width: 100, height: 2000)
sut.loadViewIfNeeded()
let firstName = sut.cell(at: IndexPath(row: 0, section: 0)).textField
firstName?.text = "Scrooge"
firstName?.sendActions(for: .editingChanged)
let lastName = sut.cell(at: IndexPath(row: 1, section: 0)).textField
lastName?.text = "McDuck"
lastName?.sendActions(for: .editingChanged)
let password = sut.cell(at: IndexPath(row: 0, section: 2)).textField
password?.text = "money"
password?.sendActions(for: .editingChanged)
let terms = sut.cell(at: IndexPath(row: 0, section: 3)).accessoryView as? UISwitch
terms?.isOn = true
terms?.sendActions(for: .valueChanged)
let saveButton = try XCTUnwrap(sut.navigationItem.rightBarButtonItem, "Found nil instead of item")
XCTAssertFalse(saveButton.isEnabled, "Item is enabled")
}
func test_shorter_than_length_6_password_disables_save_button() throws {
let sut = RegisterViewController.make(register: { _, _ in })
sut.tableView.frame = CGRect(x: 0, y: 0, width: 100, height: 2000)
sut.loadViewIfNeeded()
let firstName = sut.cell(at: IndexPath(row: 0, section: 0)).textField
firstName?.text = "Scrooge"
firstName?.sendActions(for: .editingChanged)
let lastName = sut.cell(at: IndexPath(row: 1, section: 0)).textField
lastName?.text = "McDuck"
lastName?.sendActions(for: .editingChanged)
let password = sut.cell(at: IndexPath(row: 0, section: 2)).textField
password?.text = "money"
password?.sendActions(for: .editingChanged)
let terms = sut.cell(at: IndexPath(row: 0, section: 3)).accessoryView as? UISwitch
terms?.isOn = true
terms?.sendActions(for: .valueChanged)
let saveButton = try XCTUnwrap(sut.navigationItem.rightBarButtonItem, "Found nil instead of item")
XCTAssertFalse(saveButton.isEnabled, "Item is enabled")
}
extension UITextField {
func enter(text: String) {
self.text = text
sendActions(for: .editingChanged)
}
}
func test_shorter_than_length_6_password_disables_save_button() throws {
let sut = RegisterViewController.make(register: { _, _ in })
sut.tableView.frame = CGRect(x: 0, y: 0, width: 100, height: 2000)
sut.loadViewIfNeeded()
let firstName = sut.cell(at: IndexPath(row: 0, section: 0)).textField
firstName?.text = "Scrooge"
firstName?.sendActions(for: .editingChanged)
let lastName = sut.cell(at: IndexPath(row: 1, section: 0)).textField
lastName?.text = "McDuck"
lastName?.sendActions(for: .editingChanged)
let password = sut.cell(at: IndexPath(row: 0, section: 2)).textField
password?.text = "money"
password?.sendActions(for: .editingChanged)
let terms = sut.cell(at: IndexPath(row: 0, section: 3)).accessoryView as? UISwitch
terms?.isOn = true
terms?.sendActions(for: .valueChanged)
let saveButton = try XCTUnwrap(sut.navigationItem.rightBarButtonItem, "Found nil instead of item")
XCTAssertFalse(saveButton.isEnabled, "Item is enabled")
}
func test_shorter_than_length_6_password_disables_save_button() throws {
let sut = RegisterViewController.make(register: { _, _ in })
sut.tableView.frame = CGRect(x: 0, y: 0, width: 100, height: 2000)
sut.loadViewIfNeeded()
let firstName = sut.cell(at: IndexPath(row: 0, section: 0)).textField
firstName?.enter(text: "Scrooge")
let lastName = sut.cell(at: IndexPath(row: 1, section: 0)).textField
lastName?.enter(text: "McDuck")
let password = sut.cell(at: IndexPath(row: 0, section: 2)).textField
password?.enter(text: "money")
let terms = sut.cell(at: IndexPath(row: 0, section: 3)).accessoryView as? UISwitch
terms?.isOn = true
terms?.sendActions(for: .valueChanged)
let saveButton = try XCTUnwrap(sut.navigationItem.rightBarButtonItem, "Found nil instead of item")
XCTAssertFalse(saveButton.isEnabled, "Item is enabled")
}
func test_shorter_than_length_6_password_disables_save_button() throws {
let sut = RegisterViewController.make(register: { _, _ in })
sut.tableView.frame = CGRect(x: 0, y: 0, width: 100, height: 2000)
sut.loadViewIfNeeded()
let firstName = sut.cell(at: IndexPath(row: 0, section: 0)).textField
firstName?.enter(text: "Scrooge")
let lastName = sut.cell(at: IndexPath(row: 1, section: 0)).textField
lastName?.enter(text: "McDuck")
let password = sut.cell(at: IndexPath(row: 0, section: 2)).textField
password?.enter(text: "money")
let terms = sut.cell(at: IndexPath(row: 0, section: 3)).accessoryView as? UISwitch
terms?.isOn = true
terms?.sendActions(for: .valueChanged)
let saveButton = try XCTUnwrap(sut.navigationItem.rightBarButtonItem, "Found nil instead of item")
XCTAssertFalse(saveButton.isEnabled, "Item is enabled")
}
func test_shorter_than_length_6_password_disables_save_button() throws {
let sut = RegisterViewController.make(register: { _, _ in })
sut.tableView.frame = CGRect(x: 0, y: 0, width: 100, height: 2000)
sut.loadViewIfNeeded()
let firstName = sut.cell(at: IndexPath(row: 0, section: 0)).textField
firstName?.enter(text: "Scrooge")
let lastName = sut.cell(at: IndexPath(row: 1, section: 0)).textField
lastName?.enter(text: "McDuck")
let password = sut.cell(at: IndexPath(row: 0, section: 2)).textField
password?.enter(text: "money")
let terms = sut.cell(at: IndexPath(row: 0, section: 3)).`switch`
terms?.toggle(true)
let saveButton = try XCTUnwrap(sut.navigationItem.rightBarButtonItem, "Found nil instead of item")
XCTAssertFalse(saveButton.isEnabled, "Item is enabled")
}
func test_shorter_than_length_6_password_disables_save_button() throws {
let sut = RegisterViewController.make(register: { _, _ in })
sut.tableView.frame = CGRect(x: 0, y: 0, width: 100, height: 2000)
sut.loadViewIfNeeded()
let firstName = sut.cell(at: IndexPath(row: 0, section: 0)).textField
firstName?.enter(text: "Scrooge")
let lastName = sut.cell(at: IndexPath(row: 1, section: 0)).textField
lastName?.enter(text: "McDuck")
let password = sut.cell(at: IndexPath(row: 0, section: 2)).textField
password?.enter(text: "money")
let terms = sut.cell(at: IndexPath(row: 0, section: 3)).`switch`
terms?.toggle(true)
let saveButton = try XCTUnwrap(sut.navigationItem.rightBarButtonItem, "Found nil instead of item")
XCTAssertFalse(saveButton.isEnabled, "Item is enabled")
}
IndexPath conforms to ExpressibleByArrayLiteral
public struct IndexPath: ReferenceConvertible, Equatable, Hashable, MutableCollection, RandomAccessCollection, Comparable, ExpressibleByArrayLiteral { ... }
func test_shorter_than_length_6_password_disables_save_button() throws {
let sut = RegisterViewController.make(register: { _, _ in })
sut.tableView.frame = CGRect(x: 0, y: 0, width: 100, height: 2000)
sut.loadViewIfNeeded()
let firstName = sut.cell(at: IndexPath(row: 0, section: 0)).textField
firstName?.enter(text: "Scrooge")
let lastName = sut.cell(at: IndexPath(row: 1, section: 0)).textField
lastName?.enter(text: "McDuck")
let password = sut.cell(at: IndexPath(row: 0, section: 2)).textField
password?.enter(text: "money")
let terms = sut.cell(at: IndexPath(row: 0, section: 3)).`switch`
terms?.toggle(true)
let saveButton = try XCTUnwrap(sut.navigationItem.rightBarButtonItem, "Found nil instead of item")
XCTAssertFalse(saveButton.isEnabled, "Item is enabled")
}
func test_shorter_than_length_6_password_disables_save_button() throws {
let sut = RegisterViewController.make(register: { _, _ in })
sut.tableView.frame = CGRect(x: 0, y: 0, width: 100, height: 2000)
sut.loadViewIfNeeded()
let firstName = sut.cell(at: [0, 0]).textField
firstName?.enter(text: "Scrooge")
let lastName = sut.cell(at: [0, 1]).textField
lastName?.enter(text: "McDuck")
let password = sut.cell(at: [2, 0]).textField
password?.enter(text: "money")
let terms = sut.cell(at: [3, 0]).`switch`
terms?.toggle(true)
let saveButton = try XCTUnwrap(sut.navigationItem.rightBarButtonItem, "Found nil instead of item")
XCTAssertFalse(saveButton.isEnabled, "Item is enabled")
}
extension RegisterViewController {
func enter(firstName: String, lastName: String, password: String) {
let firstName = sut.cell(at: [0, 0]).textField
firstName?.enter(text: firstName)
let lastName = sut.cell(at: [0, 1]).textField
lastName?.enter(text: lastName)
let password = sut.cell(at: [2, 0]).textField
password?.enter(text: password)
}
}
func test_shorter_than_length_6_password_disables_save_button() throws {
let sut = RegisterViewController.make(register: { _, _ in })
sut.tableView.frame = CGRect(x: 0, y: 0, width: 100, height: 2000)
sut.loadViewIfNeeded()
let firstName = sut.cell(at: [0, 0]).textField
firstName?.enter(text: "Scrooge")
let lastName = sut.cell(at: [0, 1]).textField
lastName?.enter(text: "McDuck")
let password = sut.cell(at: [2, 0]).textField
password?.enter(text: "money")
let terms = sut.cell(at: [3, 0]).`switch`
terms?.toggle(true)
let saveButton = try XCTUnwrap(sut.navigationItem.rightBarButtonItem, "Found nil instead of item")
XCTAssertFalse(saveButton.isEnabled, "Item is enabled")
}
func test_shorter_than_length_6_password_disables_save_button() throws {
let sut = RegisterViewController.make(register: { _, _ in })
sut.tableView.frame = CGRect(x: 0, y: 0, width: 100, height: 2000)
sut.loadViewIfNeeded()
sut.enter(firstName: "Scrooge", lastName: "McDuck", password: "money")
let terms = sut.cell(at: [3, 0]).`switch`
terms?.toggle(true)
let saveButton = try XCTUnwrap(sut.navigationItem.rightBarButtonItem, "Found nil instead of item")
XCTAssertFalse(saveButton.isEnabled, "Item is enabled")
}
func test_shorter_than_length_6_password_disables_save_button() throws {
let sut = RegisterViewController.make(register: { _, _ in })
sut.tableView.frame = CGRect(x: 0, y: 0, width: 100, height: 2000)
sut.loadViewIfNeeded()
sut.enter(firstName: "Scrooge", lastName: "McDuck", password: "money")
sut.termsOfService(isAccepted: true)
let saveButton = try XCTUnwrap(sut.navigationItem.rightBarButtonItem, "Found nil instead of item")
XCTAssertFalse(saveButton.isEnabled, "Item is enabled")
}
Creation Method
How do we construct the Fresh Fixture?
We set up the test fixture by calling methods that hide the mechanics of building ready-
to-use objects behind Intent-Revealing Names.
— Gerard Meszaros, xUnit Test Patterns: Refactoring
Test Code
func test_shorter_than_length_6_password_disables_save_button() throws {
let sut = RegisterViewController.make(register: { _, _ in })
sut.tableView.frame = CGRect(x: 0, y: 0, width: 100, height: 2000)
sut.loadViewIfNeeded()
sut.enter(firstName: "Scrooge", lastName: "McDuck", password: "money")
sut.termsOfService(isAccepted: true)
let saveButton = try XCTUnwrap(sut.navigationItem.rightBarButtonItem, "Found nil instead of item")
XCTAssertFalse(saveButton.isEnabled, "Item is enabled")
}
extension RegisterViewController {
static func fixture() -> RegisterViewController {
let viewController = RegisterViewController.make(register: { _, _ in })
viewController.tableView.frame = CGRect(x: 0, y: 0, width: 100, height: 2000)
viewController.loadViewIfNeeded()
return viewController
}
}
func test_shorter_than_length_6_password_disables_save_button() throws {
let sut = RegisterViewController.make(register: { _, _ in })
sut.tableView.frame = CGRect(x: 0, y: 0, width: 100, height: 2000)
sut.loadViewIfNeeded()
sut.enter(firstName: "Scrooge", lastName: "McDuck", password: "money")
sut.termsOfService(isAccepted: true)
let saveButton = try XCTUnwrap(sut.navigationItem.rightBarButtonItem, "Found nil instead of item")
XCTAssertFalse(saveButton.isEnabled, "Item is enabled")
}
func test_shorter_than_length_6_password_disables_save_button() throws {
let sut = RegisterViewController.fixture()
sut.enter(firstName: "Scrooge", lastName: "McDuck", password: "money")
sut.termsOfService(isAccepted: true)
let saveButton = try XCTUnwrap(sut.navigationItem.rightBarButtonItem, "Found nil instead of item")
XCTAssertFalse(saveButton.isEnabled, "Item is enabled")
}
extension RegisterViewController {
var saveButton: UIBarButtonItem? {
navigationItem.rightBarButtonItem
}
}
func test_shorter_than_length_6_password_disables_save_button() throws {
let sut = RegisterViewController.fixture()
sut.enter(firstName: "Scrooge", lastName: "McDuck", password: "money")
sut.termsOfService(isAccepted: true)
let saveButton = try XCTUnwrap(sut.navigationItem.rightBarButtonItem, "Found nil instead of item")
XCTAssertFalse(saveButton.isEnabled, "Item is enabled")
}
func test_shorter_than_length_6_password_disables_save_button() throws {
let sut = RegisterViewController.fixture()
sut.enter(firstName: "Scrooge", lastName: "McDuck", password: "money")
sut.termsOfService(isAccepted: true)
let saveButton = try XCTUnwrap(sut.saveButton, "Found nil instead of item")
XCTAssertFalse(saveButton.isEnabled, "Item is enabled")
}
Custom Assertion
How do we make tests self-checking when we have test-specific equality logic? How do
we reduce Test Code Duplication when the same assertion
logic appears in many tests?
How do we avoid Conditional Test Logic?
We create a purpose-built Assertion Method that compares only those attributes of the
object that define test-specific equality.
— Gerard Meszaros, xUnit Test Patterns: Refactoring
Test Code
func test_shorter_than_length_6_password_disables_save_button() throws {
let sut = RegisterViewController.fixture()
sut.enter(firstName: "Scrooge", lastName: "McDuck", password: "money")
sut.termsOfService(isAccepted: true)
let saveButton = try XCTUnwrap(sut.saveButton, "Found nil instead of item")
XCTAssertFalse(saveButton.isEnabled, "Item is enabled")
}
func assertDisabled(_ button: UIBarButtonItem?) {
guard let button = button else {
XCTFail("Found nil instead of item")
}
XCTAssertFalse(button.isEnabled, "Item is enabled")
}
func test_shorter_than_length_6_password_disables_save_button() throws {
let sut = RegisterViewController.fixture()
sut.enter(firstName: "Scrooge", lastName: "McDuck", password: "money")
sut.termsOfService(isAccepted: true)
let saveButton = try XCTUnwrap(sut.saveButton, "Found nil instead of item")
XCTAssertFalse(saveButton.isEnabled, "Item is enabled")
}
func test_shorter_than_length_6_password_disables_save_button() throws {
let sut = RegisterViewController.fixture()
sut.enter(firstName: "Scrooge", lastName: "McDuck", password: "money")
sut.termsOfService(isAccepted: true)
assertDisabled(sut.saveButton)
}
// Refactored
func test_shorter_than_length_6_password_disables_save_button() throws {
let sut = RegisterViewController.fixture()
sut.enter(firstName: "Scrooge", lastName: "McDuck", password: "money")
sut.termsOfService(isAccepted: true)
assertDisabled(sut.saveButton)
}
// Old
func test_shorter_than_length_6_password_disables_save_button() throws {
let sut = RegisterViewController.make(register: { _, _ in })
sut.tableView.frame = CGRect(x: 0, y: 0, width: 100, height: 2000)
sut.loadViewIfNeeded()
let firstName = sut.tableView.cellForRow(at: IndexPath(row: 0, section: 0)).contentView.subviews.first as? UITextField
firstName?.text = "Scrooge"
firstName?.sendActions(for: .editingChanged)
let lastName = sut.tableView.cellForRow(at: IndexPath(row: 1, section: 0)).contentView.subviews.first as? UITextField
lastName?.text = "McDuck"
lastName?.sendActions(for: .editingChanged)
let password = sut.tableView.cellForRow(at: IndexPath(row: 0, section: 2)).contentView.subviews.first as? UITextField
password?.text = "money"
password?.sendActions(for: .editingChanged)
let terms = sut.tableView.cellForRow(at: IndexPath(row: 0, section: 3)).accessoryView as? UISwitch
terms?.isOn = true
terms?.sendActions(for: .valueChanged)
let saveButton = try XCTUnwrap(sut.navigationItem.rightBarButtonItem, "Found nil instead of item")
XCTAssertFalse(saveButton.isEnabled, "Item is enabled")
}
Should we stop here?
It depends!
func test_shorter_than_length_6_password_disables_save_button() throws {
let sut = RegisterViewController.fixture()
sut.enter(firstName: "Scrooge", lastName: "McDuck", password: "money")
sut.termsOfService(isAccepted: true)
assertDisabled(sut.saveButton)
}
func test_shorter_than_length_6_password_disables_save_button() throws {
let sut = RegisterViewController.fixture()
sut.enter(firstName: "Scrooge", lastName: "McDuck", password: "m")
sut.termsOfService(isAccepted: true)
assertDisabled(sut.saveButton)
}
func test_shorter_than_length_6_password_disables_save_button() throws {
let sut = RegisterViewController.fixture()
sut.enter(firstName: "Scrooge", lastName: "McDuck", password: "")
sut.termsOfService(isAccepted: true)
assertDisabled(sut.saveButton)
}
The Rule of Three .... The third time you do
something similar, you refactor.
— Martin Fowler, Refactoring: Improving the Design
of Existing Code
Parameterized Test
How do we reduce Test Code Duplication when the same test logic appears in many
tests?
We pass the information needed to do fixture setup and result verification to a utility
method that implements the entire test life cycle.
— Gerard Meszaros, xUnit Test Patterns: Refactoring
Test Code
func test_shorter_than_length_6_password_disables_save_button() throws {
verifySaveButtonDisabled(password: "money")
verifySaveButtonDisabled(password: "m")
verifySaveButtonDisabled(password: "")
}
private func verifySaveButtonDisabled(password: String) throws {
let sut = RegisterViewController.fixture()
sut.enter(firstName: "Scrooge", lastName: "McDuck", password: password)
sut.termsOfService(isAccepted: true)
assertDisabled(sut.saveButton)
}
func test_shorter_than_length_6_password_disables_save_button() throws {
verifySaveButtonDisabled(password: "money")
verifySaveButtonDisabled(password: "m")
verifySaveButtonDisabled(password: "")
}
private func verifySaveButtonDisabled(password: String, file: StaticString = #file, line: UInt = #line) throws {
let sut = RegisterViewController.fixture()
sut.enter(firstName: "Scrooge", lastName: "McDuck", password: password)
sut.termsOfService(isAccepted: true)
assertDisabled(sut.saveButton, file: file, line: line)
}
Questions?
When is the
next meetup?
Thank you for
coming!

Annihilate test smells by refactoring to patterns

  • 1.
  • 2.
  • 3.
    https://www.programmingbooks.dev An Ordered andCurated Reading List for Software Craftsmanship Growth
  • 4.
  • 5.
    We are lookingfor team members: iOS, Web, and Embedded developers https://www.parakey.co/company/careers
  • 6.
    Annihilate test smellsby refactoring to patterns
  • 7.
    A procedure leadingto acceptance or rejection — Kent Beck, Test-Driven Development: By Example
  • 8.
    A smell isa symptom of a problem. — Gerard Meszaros, xUnit Test Patterns: Refactoring Test Code
  • 9.
    Each pattern describesa problem which occurs over and over again in our environment, and then describes the core of the solution to that problem, in such a way that you can use this solution a million times over, without ever doing it the same way twice — Christopher Alexander, A Pattern Language
  • 10.
    Refactoring (verb): torestructure software by applying a series of refactorings without changing its observable behavior. — Martin Fowler, Refactoring: Improving the Design of Existing Code
  • 11.
  • 12.
    Tests can quicklybecome a bottleneck in an agile development process. This may not be immediately obvious to those who have never experienced the difference between simple, easily understood tests and complex, obtuse, hard-to-maintain tests. — Gerard Meszaros, xUnit Test Patterns: Refactoring Test Code
  • 13.
    User Story Guest mustuse a strong password Disable save button when password is too weak (less than 6 characters)
  • 15.
    Let's do someTest-Driven Development!
  • 17.
    func test_shorter_than_length_6_password_disables_save_button() throws{ let sut = RegisterViewController.make(register: { _, _ in }) sut.tableView.frame = CGRect(x: 0, y: 0, width: 100, height: 2000) sut.loadViewIfNeeded() let firstName = sut.tableView.cellForRow(at: IndexPath(row: 0, section: 0)).contentView.subviews.first as? UITextField firstName?.text = "Scrooge" firstName?.sendActions(for: .editingChanged) let lastName = sut.tableView.cellForRow(at: IndexPath(row: 1, section: 0)).contentView.subviews.first as? UITextField lastName?.text = "McDuck" lastName?.sendActions(for: .editingChanged) let password = sut.tableView.cellForRow(at: IndexPath(row: 0, section: 2)).contentView.subviews.first as? UITextField password?.text = "money" password?.sendActions(for: .editingChanged) let terms = sut.tableView.cellForRow(at: IndexPath(row: 0, section: 3)).accessoryView as? UISwitch terms?.isOn = true terms?.sendActions(for: .valueChanged) let saveButton = try XCTUnwrap(sut.navigationItem.rightBarButtonItem, "Found nil instead of item") XCTAssertFalse(saveButton.isEnabled, "Item is enabled") }
  • 18.
    Obscure Test It isdifficult to understand the test at a glance. — Gerard Meszaros, xUnit Test Patterns: Refactoring Test Code
  • 19.
    func test_shorter_than_length_6_password_disables_save_button() throws{ let sut = RegisterViewController.make(register: { _, _ in }) sut.tableView.frame = CGRect(x: 0, y: 0, width: 100, height: 2000) sut.loadViewIfNeeded() let firstName = sut.tableView.cellForRow(at: IndexPath(row: 0, section: 0)).contentView.subviews.first as? UITextField firstName?.text = "Scrooge" firstName?.sendActions(for: .editingChanged) let lastName = sut.tableView.cellForRow(at: IndexPath(row: 1, section: 0)).contentView.subviews.first as? UITextField lastName?.text = "McDuck" lastName?.sendActions(for: .editingChanged) let password = sut.tableView.cellForRow(at: IndexPath(row: 0, section: 2)).contentView.subviews.first as? UITextField password?.text = "money" password?.sendActions(for: .editingChanged) let terms = sut.tableView.cellForRow(at: IndexPath(row: 0, section: 3)).accessoryView as? UISwitch terms?.isOn = true terms?.sendActions(for: .valueChanged) let saveButton = try XCTUnwrap(sut.navigationItem.rightBarButtonItem, "Found nil instead of item") XCTAssertFalse(saveButton.isEnabled, "Item is enabled") }
  • 20.
    Test Utility Method Howdo we reduce Test Code Duplication? We encapsulate the test logic we want to reuse behind a suitably named utility method. — Gerard Meszaros, xUnit Test Patterns: Refactoring Test Code
  • 21.
    SUT Encapsulation Method reasonfor using a Test Utility Method is to encapsulate unnecessary knowledge of the API of the SUT. What constitutes unnecessary? Any method we call on the SUT that is not the method being tested creates additional coupling between the test and the SUT. — Gerard Meszaros, xUnit Test Patterns: Refactoring Test Code
  • 22.
    func test_shorter_than_length_6_password_disables_save_button() throws{ let sut = RegisterViewController.make(register: { _, _ in }) sut.tableView.frame = CGRect(x: 0, y: 0, width: 100, height: 2000) sut.loadViewIfNeeded() let firstName = sut.tableView.cellForRow(at: IndexPath(row: 0, section: 0)).contentView.subviews.first as? UITextField firstName?.text = "Scrooge" firstName?.sendActions(for: .editingChanged) let lastName = sut.tableView.cellForRow(at: IndexPath(row: 1, section: 0)).contentView.subviews.first as? UITextField lastName?.text = "McDuck" lastName?.sendActions(for: .editingChanged) let password = sut.tableView.cellForRow(at: IndexPath(row: 0, section: 2)).contentView.subviews.first as? UITextField password?.text = "money" password?.sendActions(for: .editingChanged) let terms = sut.tableView.cellForRow(at: IndexPath(row: 0, section: 3)).accessoryView as? UISwitch terms?.isOn = true terms?.sendActions(for: .valueChanged) let saveButton = try XCTUnwrap(sut.navigationItem.rightBarButtonItem, "Found nil instead of item") XCTAssertFalse(saveButton.isEnabled, "Item is enabled") }
  • 23.
    extension UITableViewController { funccell(at indexPath: IndexPath) -> UITableViewCell? { tableView.cellForRow(at: indexPath) } }
  • 24.
    func test_shorter_than_length_6_password_disables_save_button() throws{ let sut = RegisterViewController.make(register: { _, _ in }) sut.tableView.frame = CGRect(x: 0, y: 0, width: 100, height: 2000) sut.loadViewIfNeeded() let firstName = sut.tableView.cellForRow(at: IndexPath(row: 0, section: 0)).contentView.subviews.first as? UITextField firstName?.text = "Scrooge" firstName?.sendActions(for: .editingChanged) let lastName = sut.tableView.cellForRow(at: IndexPath(row: 1, section: 0)).contentView.subviews.first as? UITextField lastName?.text = "McDuck" lastName?.sendActions(for: .editingChanged) let password = sut.tableView.cellForRow(at: IndexPath(row: 0, section: 2)).contentView.subviews.first as? UITextField password?.text = "money" password?.sendActions(for: .editingChanged) let terms = sut.tableView.cellForRow(at: IndexPath(row: 0, section: 3)).accessoryView as? UISwitch terms?.isOn = true terms?.sendActions(for: .valueChanged) let saveButton = try XCTUnwrap(sut.navigationItem.rightBarButtonItem, "Found nil instead of item") XCTAssertFalse(saveButton.isEnabled, "Item is enabled") }
  • 25.
    func test_shorter_than_length_6_password_disables_save_button() throws{ let sut = RegisterViewController.make(register: { _, _ in }) sut.tableView.frame = CGRect(x: 0, y: 0, width: 100, height: 2000) sut.loadViewIfNeeded() let firstName = sut.cell(at: IndexPath(row: 0, section: 0)).contentView.subviews.first as? UITextField firstName?.text = "Scrooge" firstName?.sendActions(for: .editingChanged) let lastName = sut.cell(at: IndexPath(row: 1, section: 0)).contentView.subviews.first as? UITextField lastName?.text = "McDuck" lastName?.sendActions(for: .editingChanged) let password = sut.cell(at: IndexPath(row: 0, section: 2)).contentView.subviews.first as? UITextField password?.text = "money" password?.sendActions(for: .editingChanged) let terms = sut.cell(at: IndexPath(row: 0, section: 3)).accessoryView as? UISwitch terms?.isOn = true terms?.sendActions(for: .valueChanged) let saveButton = try XCTUnwrap(sut.navigationItem.rightBarButtonItem, "Found nil instead of item") XCTAssertFalse(saveButton.isEnabled, "Item is enabled") }
  • 26.
    func test_shorter_than_length_6_password_disables_save_button() throws{ let sut = RegisterViewController.make(register: { _, _ in }) sut.tableView.frame = CGRect(x: 0, y: 0, width: 100, height: 2000) sut.loadViewIfNeeded() let firstName = sut.cell(at: IndexPath(row: 0, section: 0)).contentView.subviews.first as? UITextField firstName?.text = "Scrooge" firstName?.sendActions(for: .editingChanged) let lastName = sut.cell(at: IndexPath(row: 1, section: 0)).contentView.subviews.first as? UITextField lastName?.text = "McDuck" lastName?.sendActions(for: .editingChanged) let password = sut.cell(at: IndexPath(row: 0, section: 2)).contentView.subviews.first as? UITextField password?.text = "money" password?.sendActions(for: .editingChanged) let terms = sut.cell(at: IndexPath(row: 0, section: 3)).accessoryView as? UISwitch terms?.isOn = true terms?.sendActions(for: .valueChanged) let saveButton = try XCTUnwrap(sut.navigationItem.rightBarButtonItem, "Found nil instead of item") XCTAssertFalse(saveButton.isEnabled, "Item is enabled") }
  • 27.
    extension UITableViewCell { vartextField: UITextField? { contentView.subviews.first as? UITextField } }
  • 28.
    func test_shorter_than_length_6_password_disables_save_button() throws{ let sut = RegisterViewController.make(register: { _, _ in }) sut.tableView.frame = CGRect(x: 0, y: 0, width: 100, height: 2000) sut.loadViewIfNeeded() let firstName = sut.cell(at: IndexPath(row: 0, section: 0)).contentView.subviews.first as? UITextField firstName?.text = "Scrooge" firstName?.sendActions(for: .editingChanged) let lastName = sut.cell(at: IndexPath(row: 1, section: 0)).contentView.subviews.first as? UITextField lastName?.text = "McDuck" lastName?.sendActions(for: .editingChanged) let password = sut.cell(at: IndexPath(row: 0, section: 2)).contentView.subviews.first as? UITextField password?.text = "money" password?.sendActions(for: .editingChanged) let terms = sut.cell(at: IndexPath(row: 0, section: 3)).accessoryView as? UISwitch terms?.isOn = true terms?.sendActions(for: .valueChanged) let saveButton = try XCTUnwrap(sut.navigationItem.rightBarButtonItem, "Found nil instead of item") XCTAssertFalse(saveButton.isEnabled, "Item is enabled") }
  • 29.
    func test_shorter_than_length_6_password_disables_save_button() throws{ let sut = RegisterViewController.make(register: { _, _ in }) sut.tableView.frame = CGRect(x: 0, y: 0, width: 100, height: 2000) sut.loadViewIfNeeded() let firstName = sut.cell(at: IndexPath(row: 0, section: 0)).textField firstName?.text = "Scrooge" firstName?.sendActions(for: .editingChanged) let lastName = sut.cell(at: IndexPath(row: 1, section: 0)).textField lastName?.text = "McDuck" lastName?.sendActions(for: .editingChanged) let password = sut.cell(at: IndexPath(row: 0, section: 2)).textField password?.text = "money" password?.sendActions(for: .editingChanged) let terms = sut.cell(at: IndexPath(row: 0, section: 3)).accessoryView as? UISwitch terms?.isOn = true terms?.sendActions(for: .valueChanged) let saveButton = try XCTUnwrap(sut.navigationItem.rightBarButtonItem, "Found nil instead of item") XCTAssertFalse(saveButton.isEnabled, "Item is enabled") }
  • 30.
    func test_shorter_than_length_6_password_disables_save_button() throws{ let sut = RegisterViewController.make(register: { _, _ in }) sut.tableView.frame = CGRect(x: 0, y: 0, width: 100, height: 2000) sut.loadViewIfNeeded() let firstName = sut.cell(at: IndexPath(row: 0, section: 0)).textField firstName?.text = "Scrooge" firstName?.sendActions(for: .editingChanged) let lastName = sut.cell(at: IndexPath(row: 1, section: 0)).textField lastName?.text = "McDuck" lastName?.sendActions(for: .editingChanged) let password = sut.cell(at: IndexPath(row: 0, section: 2)).textField password?.text = "money" password?.sendActions(for: .editingChanged) let terms = sut.cell(at: IndexPath(row: 0, section: 3)).accessoryView as? UISwitch terms?.isOn = true terms?.sendActions(for: .valueChanged) let saveButton = try XCTUnwrap(sut.navigationItem.rightBarButtonItem, "Found nil instead of item") XCTAssertFalse(saveButton.isEnabled, "Item is enabled") }
  • 31.
    extension UITextField { funcenter(text: String) { self.text = text sendActions(for: .editingChanged) } }
  • 32.
    func test_shorter_than_length_6_password_disables_save_button() throws{ let sut = RegisterViewController.make(register: { _, _ in }) sut.tableView.frame = CGRect(x: 0, y: 0, width: 100, height: 2000) sut.loadViewIfNeeded() let firstName = sut.cell(at: IndexPath(row: 0, section: 0)).textField firstName?.text = "Scrooge" firstName?.sendActions(for: .editingChanged) let lastName = sut.cell(at: IndexPath(row: 1, section: 0)).textField lastName?.text = "McDuck" lastName?.sendActions(for: .editingChanged) let password = sut.cell(at: IndexPath(row: 0, section: 2)).textField password?.text = "money" password?.sendActions(for: .editingChanged) let terms = sut.cell(at: IndexPath(row: 0, section: 3)).accessoryView as? UISwitch terms?.isOn = true terms?.sendActions(for: .valueChanged) let saveButton = try XCTUnwrap(sut.navigationItem.rightBarButtonItem, "Found nil instead of item") XCTAssertFalse(saveButton.isEnabled, "Item is enabled") }
  • 33.
    func test_shorter_than_length_6_password_disables_save_button() throws{ let sut = RegisterViewController.make(register: { _, _ in }) sut.tableView.frame = CGRect(x: 0, y: 0, width: 100, height: 2000) sut.loadViewIfNeeded() let firstName = sut.cell(at: IndexPath(row: 0, section: 0)).textField firstName?.enter(text: "Scrooge") let lastName = sut.cell(at: IndexPath(row: 1, section: 0)).textField lastName?.enter(text: "McDuck") let password = sut.cell(at: IndexPath(row: 0, section: 2)).textField password?.enter(text: "money") let terms = sut.cell(at: IndexPath(row: 0, section: 3)).accessoryView as? UISwitch terms?.isOn = true terms?.sendActions(for: .valueChanged) let saveButton = try XCTUnwrap(sut.navigationItem.rightBarButtonItem, "Found nil instead of item") XCTAssertFalse(saveButton.isEnabled, "Item is enabled") }
  • 34.
    func test_shorter_than_length_6_password_disables_save_button() throws{ let sut = RegisterViewController.make(register: { _, _ in }) sut.tableView.frame = CGRect(x: 0, y: 0, width: 100, height: 2000) sut.loadViewIfNeeded() let firstName = sut.cell(at: IndexPath(row: 0, section: 0)).textField firstName?.enter(text: "Scrooge") let lastName = sut.cell(at: IndexPath(row: 1, section: 0)).textField lastName?.enter(text: "McDuck") let password = sut.cell(at: IndexPath(row: 0, section: 2)).textField password?.enter(text: "money") let terms = sut.cell(at: IndexPath(row: 0, section: 3)).accessoryView as? UISwitch terms?.isOn = true terms?.sendActions(for: .valueChanged) let saveButton = try XCTUnwrap(sut.navigationItem.rightBarButtonItem, "Found nil instead of item") XCTAssertFalse(saveButton.isEnabled, "Item is enabled") }
  • 35.
    func test_shorter_than_length_6_password_disables_save_button() throws{ let sut = RegisterViewController.make(register: { _, _ in }) sut.tableView.frame = CGRect(x: 0, y: 0, width: 100, height: 2000) sut.loadViewIfNeeded() let firstName = sut.cell(at: IndexPath(row: 0, section: 0)).textField firstName?.enter(text: "Scrooge") let lastName = sut.cell(at: IndexPath(row: 1, section: 0)).textField lastName?.enter(text: "McDuck") let password = sut.cell(at: IndexPath(row: 0, section: 2)).textField password?.enter(text: "money") let terms = sut.cell(at: IndexPath(row: 0, section: 3)).`switch` terms?.toggle(true) let saveButton = try XCTUnwrap(sut.navigationItem.rightBarButtonItem, "Found nil instead of item") XCTAssertFalse(saveButton.isEnabled, "Item is enabled") }
  • 36.
    func test_shorter_than_length_6_password_disables_save_button() throws{ let sut = RegisterViewController.make(register: { _, _ in }) sut.tableView.frame = CGRect(x: 0, y: 0, width: 100, height: 2000) sut.loadViewIfNeeded() let firstName = sut.cell(at: IndexPath(row: 0, section: 0)).textField firstName?.enter(text: "Scrooge") let lastName = sut.cell(at: IndexPath(row: 1, section: 0)).textField lastName?.enter(text: "McDuck") let password = sut.cell(at: IndexPath(row: 0, section: 2)).textField password?.enter(text: "money") let terms = sut.cell(at: IndexPath(row: 0, section: 3)).`switch` terms?.toggle(true) let saveButton = try XCTUnwrap(sut.navigationItem.rightBarButtonItem, "Found nil instead of item") XCTAssertFalse(saveButton.isEnabled, "Item is enabled") }
  • 37.
    IndexPath conforms toExpressibleByArrayLiteral public struct IndexPath: ReferenceConvertible, Equatable, Hashable, MutableCollection, RandomAccessCollection, Comparable, ExpressibleByArrayLiteral { ... }
  • 38.
    func test_shorter_than_length_6_password_disables_save_button() throws{ let sut = RegisterViewController.make(register: { _, _ in }) sut.tableView.frame = CGRect(x: 0, y: 0, width: 100, height: 2000) sut.loadViewIfNeeded() let firstName = sut.cell(at: IndexPath(row: 0, section: 0)).textField firstName?.enter(text: "Scrooge") let lastName = sut.cell(at: IndexPath(row: 1, section: 0)).textField lastName?.enter(text: "McDuck") let password = sut.cell(at: IndexPath(row: 0, section: 2)).textField password?.enter(text: "money") let terms = sut.cell(at: IndexPath(row: 0, section: 3)).`switch` terms?.toggle(true) let saveButton = try XCTUnwrap(sut.navigationItem.rightBarButtonItem, "Found nil instead of item") XCTAssertFalse(saveButton.isEnabled, "Item is enabled") }
  • 39.
    func test_shorter_than_length_6_password_disables_save_button() throws{ let sut = RegisterViewController.make(register: { _, _ in }) sut.tableView.frame = CGRect(x: 0, y: 0, width: 100, height: 2000) sut.loadViewIfNeeded() let firstName = sut.cell(at: [0, 0]).textField firstName?.enter(text: "Scrooge") let lastName = sut.cell(at: [0, 1]).textField lastName?.enter(text: "McDuck") let password = sut.cell(at: [2, 0]).textField password?.enter(text: "money") let terms = sut.cell(at: [3, 0]).`switch` terms?.toggle(true) let saveButton = try XCTUnwrap(sut.navigationItem.rightBarButtonItem, "Found nil instead of item") XCTAssertFalse(saveButton.isEnabled, "Item is enabled") }
  • 40.
    extension RegisterViewController { funcenter(firstName: String, lastName: String, password: String) { let firstName = sut.cell(at: [0, 0]).textField firstName?.enter(text: firstName) let lastName = sut.cell(at: [0, 1]).textField lastName?.enter(text: lastName) let password = sut.cell(at: [2, 0]).textField password?.enter(text: password) } }
  • 41.
    func test_shorter_than_length_6_password_disables_save_button() throws{ let sut = RegisterViewController.make(register: { _, _ in }) sut.tableView.frame = CGRect(x: 0, y: 0, width: 100, height: 2000) sut.loadViewIfNeeded() let firstName = sut.cell(at: [0, 0]).textField firstName?.enter(text: "Scrooge") let lastName = sut.cell(at: [0, 1]).textField lastName?.enter(text: "McDuck") let password = sut.cell(at: [2, 0]).textField password?.enter(text: "money") let terms = sut.cell(at: [3, 0]).`switch` terms?.toggle(true) let saveButton = try XCTUnwrap(sut.navigationItem.rightBarButtonItem, "Found nil instead of item") XCTAssertFalse(saveButton.isEnabled, "Item is enabled") }
  • 42.
    func test_shorter_than_length_6_password_disables_save_button() throws{ let sut = RegisterViewController.make(register: { _, _ in }) sut.tableView.frame = CGRect(x: 0, y: 0, width: 100, height: 2000) sut.loadViewIfNeeded() sut.enter(firstName: "Scrooge", lastName: "McDuck", password: "money") let terms = sut.cell(at: [3, 0]).`switch` terms?.toggle(true) let saveButton = try XCTUnwrap(sut.navigationItem.rightBarButtonItem, "Found nil instead of item") XCTAssertFalse(saveButton.isEnabled, "Item is enabled") }
  • 43.
    func test_shorter_than_length_6_password_disables_save_button() throws{ let sut = RegisterViewController.make(register: { _, _ in }) sut.tableView.frame = CGRect(x: 0, y: 0, width: 100, height: 2000) sut.loadViewIfNeeded() sut.enter(firstName: "Scrooge", lastName: "McDuck", password: "money") sut.termsOfService(isAccepted: true) let saveButton = try XCTUnwrap(sut.navigationItem.rightBarButtonItem, "Found nil instead of item") XCTAssertFalse(saveButton.isEnabled, "Item is enabled") }
  • 44.
    Creation Method How dowe construct the Fresh Fixture? We set up the test fixture by calling methods that hide the mechanics of building ready- to-use objects behind Intent-Revealing Names. — Gerard Meszaros, xUnit Test Patterns: Refactoring Test Code
  • 45.
    func test_shorter_than_length_6_password_disables_save_button() throws{ let sut = RegisterViewController.make(register: { _, _ in }) sut.tableView.frame = CGRect(x: 0, y: 0, width: 100, height: 2000) sut.loadViewIfNeeded() sut.enter(firstName: "Scrooge", lastName: "McDuck", password: "money") sut.termsOfService(isAccepted: true) let saveButton = try XCTUnwrap(sut.navigationItem.rightBarButtonItem, "Found nil instead of item") XCTAssertFalse(saveButton.isEnabled, "Item is enabled") }
  • 46.
    extension RegisterViewController { staticfunc fixture() -> RegisterViewController { let viewController = RegisterViewController.make(register: { _, _ in }) viewController.tableView.frame = CGRect(x: 0, y: 0, width: 100, height: 2000) viewController.loadViewIfNeeded() return viewController } }
  • 47.
    func test_shorter_than_length_6_password_disables_save_button() throws{ let sut = RegisterViewController.make(register: { _, _ in }) sut.tableView.frame = CGRect(x: 0, y: 0, width: 100, height: 2000) sut.loadViewIfNeeded() sut.enter(firstName: "Scrooge", lastName: "McDuck", password: "money") sut.termsOfService(isAccepted: true) let saveButton = try XCTUnwrap(sut.navigationItem.rightBarButtonItem, "Found nil instead of item") XCTAssertFalse(saveButton.isEnabled, "Item is enabled") }
  • 48.
    func test_shorter_than_length_6_password_disables_save_button() throws{ let sut = RegisterViewController.fixture() sut.enter(firstName: "Scrooge", lastName: "McDuck", password: "money") sut.termsOfService(isAccepted: true) let saveButton = try XCTUnwrap(sut.navigationItem.rightBarButtonItem, "Found nil instead of item") XCTAssertFalse(saveButton.isEnabled, "Item is enabled") }
  • 49.
    extension RegisterViewController { varsaveButton: UIBarButtonItem? { navigationItem.rightBarButtonItem } }
  • 50.
    func test_shorter_than_length_6_password_disables_save_button() throws{ let sut = RegisterViewController.fixture() sut.enter(firstName: "Scrooge", lastName: "McDuck", password: "money") sut.termsOfService(isAccepted: true) let saveButton = try XCTUnwrap(sut.navigationItem.rightBarButtonItem, "Found nil instead of item") XCTAssertFalse(saveButton.isEnabled, "Item is enabled") }
  • 51.
    func test_shorter_than_length_6_password_disables_save_button() throws{ let sut = RegisterViewController.fixture() sut.enter(firstName: "Scrooge", lastName: "McDuck", password: "money") sut.termsOfService(isAccepted: true) let saveButton = try XCTUnwrap(sut.saveButton, "Found nil instead of item") XCTAssertFalse(saveButton.isEnabled, "Item is enabled") }
  • 52.
    Custom Assertion How dowe make tests self-checking when we have test-specific equality logic? How do we reduce Test Code Duplication when the same assertion logic appears in many tests? How do we avoid Conditional Test Logic? We create a purpose-built Assertion Method that compares only those attributes of the object that define test-specific equality. — Gerard Meszaros, xUnit Test Patterns: Refactoring Test Code
  • 53.
    func test_shorter_than_length_6_password_disables_save_button() throws{ let sut = RegisterViewController.fixture() sut.enter(firstName: "Scrooge", lastName: "McDuck", password: "money") sut.termsOfService(isAccepted: true) let saveButton = try XCTUnwrap(sut.saveButton, "Found nil instead of item") XCTAssertFalse(saveButton.isEnabled, "Item is enabled") }
  • 54.
    func assertDisabled(_ button:UIBarButtonItem?) { guard let button = button else { XCTFail("Found nil instead of item") } XCTAssertFalse(button.isEnabled, "Item is enabled") }
  • 55.
    func test_shorter_than_length_6_password_disables_save_button() throws{ let sut = RegisterViewController.fixture() sut.enter(firstName: "Scrooge", lastName: "McDuck", password: "money") sut.termsOfService(isAccepted: true) let saveButton = try XCTUnwrap(sut.saveButton, "Found nil instead of item") XCTAssertFalse(saveButton.isEnabled, "Item is enabled") }
  • 56.
    func test_shorter_than_length_6_password_disables_save_button() throws{ let sut = RegisterViewController.fixture() sut.enter(firstName: "Scrooge", lastName: "McDuck", password: "money") sut.termsOfService(isAccepted: true) assertDisabled(sut.saveButton) }
  • 57.
    // Refactored func test_shorter_than_length_6_password_disables_save_button()throws { let sut = RegisterViewController.fixture() sut.enter(firstName: "Scrooge", lastName: "McDuck", password: "money") sut.termsOfService(isAccepted: true) assertDisabled(sut.saveButton) } // Old func test_shorter_than_length_6_password_disables_save_button() throws { let sut = RegisterViewController.make(register: { _, _ in }) sut.tableView.frame = CGRect(x: 0, y: 0, width: 100, height: 2000) sut.loadViewIfNeeded() let firstName = sut.tableView.cellForRow(at: IndexPath(row: 0, section: 0)).contentView.subviews.first as? UITextField firstName?.text = "Scrooge" firstName?.sendActions(for: .editingChanged) let lastName = sut.tableView.cellForRow(at: IndexPath(row: 1, section: 0)).contentView.subviews.first as? UITextField lastName?.text = "McDuck" lastName?.sendActions(for: .editingChanged) let password = sut.tableView.cellForRow(at: IndexPath(row: 0, section: 2)).contentView.subviews.first as? UITextField password?.text = "money" password?.sendActions(for: .editingChanged) let terms = sut.tableView.cellForRow(at: IndexPath(row: 0, section: 3)).accessoryView as? UISwitch terms?.isOn = true terms?.sendActions(for: .valueChanged) let saveButton = try XCTUnwrap(sut.navigationItem.rightBarButtonItem, "Found nil instead of item") XCTAssertFalse(saveButton.isEnabled, "Item is enabled") }
  • 58.
  • 59.
  • 60.
    func test_shorter_than_length_6_password_disables_save_button() throws{ let sut = RegisterViewController.fixture() sut.enter(firstName: "Scrooge", lastName: "McDuck", password: "money") sut.termsOfService(isAccepted: true) assertDisabled(sut.saveButton) } func test_shorter_than_length_6_password_disables_save_button() throws { let sut = RegisterViewController.fixture() sut.enter(firstName: "Scrooge", lastName: "McDuck", password: "m") sut.termsOfService(isAccepted: true) assertDisabled(sut.saveButton) } func test_shorter_than_length_6_password_disables_save_button() throws { let sut = RegisterViewController.fixture() sut.enter(firstName: "Scrooge", lastName: "McDuck", password: "") sut.termsOfService(isAccepted: true) assertDisabled(sut.saveButton) }
  • 61.
    The Rule ofThree .... The third time you do something similar, you refactor. — Martin Fowler, Refactoring: Improving the Design of Existing Code
  • 62.
    Parameterized Test How dowe reduce Test Code Duplication when the same test logic appears in many tests? We pass the information needed to do fixture setup and result verification to a utility method that implements the entire test life cycle. — Gerard Meszaros, xUnit Test Patterns: Refactoring Test Code
  • 63.
    func test_shorter_than_length_6_password_disables_save_button() throws{ verifySaveButtonDisabled(password: "money") verifySaveButtonDisabled(password: "m") verifySaveButtonDisabled(password: "") } private func verifySaveButtonDisabled(password: String) throws { let sut = RegisterViewController.fixture() sut.enter(firstName: "Scrooge", lastName: "McDuck", password: password) sut.termsOfService(isAccepted: true) assertDisabled(sut.saveButton) }
  • 64.
    func test_shorter_than_length_6_password_disables_save_button() throws{ verifySaveButtonDisabled(password: "money") verifySaveButtonDisabled(password: "m") verifySaveButtonDisabled(password: "") } private func verifySaveButtonDisabled(password: String, file: StaticString = #file, line: UInt = #line) throws { let sut = RegisterViewController.fixture() sut.enter(firstName: "Scrooge", lastName: "McDuck", password: password) sut.termsOfService(isAccepted: true) assertDisabled(sut.saveButton, file: file, line: line) }
  • 65.
  • 66.
  • 67.