patterns & practices for unit-testing swift-ly · unit testing a unit test is an automated...

64
Patterns & practices for unit-testing Swift-ly Jakub Turek 18th June, 2018

Upload: others

Post on 12-Mar-2020

13 views

Category:

Documents


0 download

TRANSCRIPT

Patterns & practices for unit-testing Swift-ly

Jakub Turek18th June, 2018

About me

Jakub Turek

https://jakubturek.com @KubaTurek turekj EL Passion

1

Agenda

1. Introduction to unit-testing.▶ Test Driven Development.▶ Reasons (not) to use TDD approach.

2. Engineering for testability.3. Common obstacles:

▶ view controllers,▶ global functions.

4. Auto-generating test code.5. Metrics.

2

Introduction to testing

Unit testing

A unit test is an automated piece of code that invokesa unit of work in the system and then checks a singleassumption about the behavior of that unit of work.

— Roy Osherove, The art of unit testing

3

Good unit test

Traits of a good unit test:

▶ automated,▶ fast,▶ tests a single logical concept in the system,▶ trustworthy,▶ readable,▶ maintanable.

4

TDD

TDD is a programming technique which combines wri-ting a test before writing just enough production codeto fulfill that test and refactoring.

— Kent Beck, Test-Driven Development by example

5

Typical response to TDD

We don’t do TDD because:

1. We are not allowed to.2. We don’t have enough time.3. It didn’t work for us*.

* Might indicate feedback loop problems.

6

TDD workflow

1. Red - write a failing test.2. Green - write minimal amount of code to pass.3. Refactor.

7

3 laws of TDD

1. You can’t write any production code until you writea failing unit test.

2. You can’t write more of a unit test than is sufficient to fail.Not compiling is failing.

3. You can’t write more production code than is sufficient topass currently failing unit test.

8

Demo

9

TDD research

Research carrier Effort QualityN. Nagappan¹

+15..35%-40..90%

Microsoft & IBM less defectsB. George, L. Williams²

+16%+18%

NCSU test cases passed

¹ Realizing quality improvement through test driven development² An Initial Investigation of Test Driven Development in Industry

10

Engineering for testability

Pure functions

How to write testable code?

1. Pass values to functions.2. Return values from functions.

— @mdiep, Matt Diephouse, Twitter

11

Boundaries

Boundaries by Gary Bernhardt

This talk is about using simple values (as opposed to complexobjects) not just for holding data, but also as the boundariesbetween components and subsystems.

https://destroyallsoftware.com/talks/boundaries

12

Imperative shell, functional core

Imperative shell:

▶ real worlddependencies,

▶ side-effects,▶ stateful operations.

Functional core:

▶ decisions,▶ purely functional

transformations.

13

Demo

14

Other appliances

Developers use this approach already:

▶ Redux-like architecture:▶ ReSwift,▶ RxFeedback.

▶ View models and data bindings:▶ RxCocoa,▶ ReactiveCocoa.

15

Resources

Controlling Complexity in Swifthttps://academy.realm.io/posts/andy-matuschak-controlling-complexity/

Enemy of the Statehttps://speakerdeck.com/jspahrsummers/enemy-of-the-state

Imperative Shell, Functional Core Gisthttps://tinyurl.com/y9cxblm8

Imperative Shell, Functional Core on iOShttps://jakubturek.com/imperative-shell-functional-core/

16

Dealing with massive controllers

Metrics (1/5)

Examine the largest files:

find Sources -name '*.swift' \| xargs wc -l \| sort -r

17

Metrics (2/5)

Output:

??? AllAssemblies.generated.swift??? ContentController.swift??? PickerController.swift??? MapController.swift??? ClassCell.swift??? CalendarController.swift??? ContactController.swift??? AuthorizeController.swift

18

Metrics (3/5)

Actual line counts:

148 AllAssemblies.generated.swift140 ContentController.swift139 PickerController.swift138 MapController.swift138 ClassCell.swift138 CalendarController.swift137 ContactController.swift135 AuthorizeController.swift

19

Metrics (4/5)

Examine the average controller size:

cloc Modules \--include-lang=Swift \--match-f='.+Controller\.swift'

20

Metrics (5/5)

▶ Controllers in total: 164.▶ Screens in total: 38.▶ Average 4.32 controller per screen.▶ Lines in total: 12327.▶ Average 75.16 lines of code per controller.

21

More View Controllers

How to decouple controllers?

Never Subclass as a last resort. Add isolated child viewcontrollers inside a parent:

1. Create a protocol for child controller boundaries.2. Inside a parent refer to a child with UIViewController& ChildProtocol type.

3. Inject child factory into a parent controller.

22

Adding a child controller (1/2)

func embed(child: UIViewController,inside view: UIView) {

addChildViewController(child)view.addSubview(child.view)

child.snp.makeConstraints {$0.edges.equalToSuperview()

}

child.didMove(toParentViewController: self)}

23

Adding a child controller (2/2)

func unembed(child: UIViewController) {child.willMove(toParentViewController: nil)child.view.removeFromSuperview()child.removeFromParentViewController()

}

24

Decoupling controllers (1/2)

protocol ChildControllerType: class {var onProductSelected: ((String) -> Void)?

}

class ChildController: UIViewController,ChildControllerType {

init(/* dependencies */) { /* ... */ }

var onProductSelected: ((String) -> Void)?

override func viewDidLoad() {/* implementation */

}}

25

Decoupling controllers (2/2)

typealias ChildControlling =UIViewController & ChildControllerType

class ParentController: UIViewController {init(factory: () -> ChildControlling) {self.factory = factory

}

override func viewDidLoad() {super.viewDidLoad()embed(child: child, inside: view)

}

private lazy var child = factory()private let factory: () -> ChildControlling

} 26

Stubbing a child controller (1/2)

class ChildControllerStub: UIViewController,ChildControllerType {

var onProductSelected: ((String) -> Void)?}

27

Stubbing a child controller (2/2)

class ParentControllerTests: XCTestCase {var childStub: ChildControllerStub!var sut: ParentController!

override func setUp() {super.setUp()childStub = ChildControllerStub()sut = ParentController(factory: { childStub })

}

func testThatSelectedProductNameIsDisplayed() {childStub.onProductSelected?("Water")XCTAssertEqual(sut.view.label.text, "Water")

}}

28

Child view controller examples

▶ Buttons:▶ perform side-effects, expose results,▶ error handling,▶ disable interaction during asynchronous operations.

▶ Forms:▶ user input validation,▶ complex presentation,▶ error handling.

▶ API data coordination. Immutable children for presenting:▶ data,▶ empty state,▶ error.

29

Global methods

Global method example (1/2)

extension UIImage {static func with(color: UIColor) -> UIImage? {let origin = CGPoint(x: 0, y: 0)let size = CGSize(width: 1, height: 1)let rect = CGRect(origin: origin, size: size)

return CGContext.drawImage(of: size) { ctx inctx.setFillColor(color.cgColor)ctx.fill(rect)

}}

}

30

Global method example (2/2)

static func drawImage(of size: CGSize,using drawer: (CGContext) -> Void)-> UIImage? {

UIGraphicsBeginImage...(size, false, 0.0)

defer { UIGraphicsEndImageContext() }

guard let ctx = UIGraphicsGetCurrentContext() else {return nil

}

drawer(ctx)

return UIGraphicsGetImage...()}

31

Swift namespace resolution (1/2)

Shipping your own controller type:

class UIViewController {func theTimeHasComeToLoadAView() {myOwnPersonalView = SomeView()

}}

let controller = UIViewController()controller.theTimeHasComeToLoadAView() // works

32

Swift namespace resolution (2/2)

Shipping your own controller library:

# Podfilepod 'MyOwnController', '~> 0.1'

// SourceFile.swiftimport MyOwnControllerimport UIKit

UIViewController() // compilation error

33

Testing global methods (1/2)

import UIKit

var imageFromContext: () -> UIImage? =UIKit.UIGraphicsGetImageFromCurrentImageContext

func UIGraphicsGetImageFromCurrentImageContext()-> UIImage? {

return imageFromContext()}

34

Testing global methods (2/2)

override func setUp() {imageFromContext = { UIImage.catMeme }

}

override func tearDown() {imageFromContext = UIKit.UIGraphicsGetImage...

}

func testThatDrawImageReturnsImageFromContext() {let image = CGContext.drawImage(of: .zero) { _ in }

XCTAssertEqual(UIImagePNGRepresentation(image),UIImagePNGRepresentation(imageFromContext())

)} 35

Test code generation

Sourcery

Sourcery

Sourcery is a code generator for Swift language, built on top ofApple’s own SourceKit. It extends the language abstractions toallow you to generate boilerplate code automatically.

https://github.com/krzysztofzablocki/Sourcery

36

Problem: equality check

var sut: Account!

override func setUp() {super.setUp()sut = try! JSONDecoder.default.decode(

Account.self,from: Bundle.jsonData(file: "account.json")

)}

func testThatAccountIsDeserialized() {XCTAssertEqual(sut.id, 32)XCTAssertEqual(sut.email, "[email protected]")/* and so on... */

}37

Account+AutoEquatable.swift

Equatable conformance can be automatically synthesized ina test target.

protocol AutoEquatable {}

extension Account: AutoEquatable {}

38

AutoEquatable.generated.swift

public func == (lhs: Account,rhs: Account) -> Bool {

guard lhs.id == rhs.id else { return false }guard lhs.email == rhs.email else {return false

}guard compareOptionals(lhs: lhs.firstName,rhs: rhs.firstName,compare: ==

) else { return false }/* and so on... */return true

}39

AutoEquatable vs. auto-synthesized Equatable

Sourcery’sAutoEquatable:▶ Can be declared as an

extension.▶ Required additional

setup to work.▶ Conformance can be

asserted.

Automatically synthesizedEquatable:▶ Introduced in Swift 4.1

(Xcode 9.3).▶ Cannot be declared as

an extension.▶ Featuresassociatedtype.

40

Problem: required initializers

0% code coverage when not using inteface builder

required init?(coder aDecoder: NSCoder) {return nil

}

41

AllViews.stencil

extension UIView {

static var allInitializers: [(NSCoder) -> UIView?] {return [{% for view in types.classes where view.based.UIView %}{% set spacer %}{% if not forloop.last %},{% endif %}{% endset %}{% for initializer in view.initializers %}{% if initializer.selectorName == "init(coder:)" %}{{ view.name }}.init(coder:){{ spacer }}{% endif %}{% endfor %}{% endfor %}]

}

}

42

AllViews.generated.swift

extension UIView {

static var allInitializers: [(NSCoder) -> UIView?] {return [

ActionBarView.init(coder:),AuthorizeErrorView.init(coder:),BlurView.init(coder:),BorderedButtonView.init(coder:),FilterView.init(coder:),HeaderView.init(coder:),/* ... */

]}

}

43

AllViewsTests.swift

func testThatAllViewsAreNonCodable() {UIView.allInitializers.forEach { initializer inXCTAssertNil(initializer(NSCoder()))

}}

44

Other usages

▶ Mock object generation.▶ Complex assertions:

▶ There is a factory class for every Route.▶ There is an integration test for every request.

45

Metrics

Metrics

Measured on every pull request:

▶ Project size:▶ lines of code,▶ files count,▶ average file size.

▶ Static analysis:▶ SwiftLint: consistent coding style,▶ jscpd: automatic copy-paste detection.

▶ Code coverage.

46

Metrics - visualization

Danger

Danger runs during your CI process, and gives teams thechance to automate common code review chores.

http://danger.systems/js/swift.html

47

Automatic copy-paste detection

JSCPD

jscpd is a tool for detect copy/paste ”design pattern” inprogramming source code.

https://github.com/kucherenko/jscpd

48

JSCPD + Danger = (1/3)

49

JSCPD + Danger = (2/3)

languages:- swift

files:- "Sources/**"

exclude:- "**/*.generated.swift"

reporter: jsonoutput: jscpd_report.json

50

JSCPD + Danger = (3/3)

def find_duplicates`jscpd`

rep = JSON.parse(File.read('jscpd_report.json'))clones = rep["statistics"]["clones"]

if clones > 0warn("JSCPD found #{clones} clone(s)")dups = rep["duplicates"]out = dups.map { |d| format_duplicate d }.join("\n")markdown("## JSCPD issues\n#{out}")

endend

Full version: https://tinyurl.com/yc23t4mb

51

Code coverage

Code coverage is a percentage of code which is covered byautomated tests.

Test coverage is a useful tool for finding untested partsof a codebase. Test coverage is of little use as a numericstatement of how good your tests are.

— Martin Fowler, TestCoverage

52

danger-xcov (1/2)

danger-xcov

danger-xcov is the Danger plugin of xcov, a friendlyvisualizer for Xcode’s code coverage files.

https://github.com/nakiostudio/danger-xcov

53

danger-xcov (2/2)

54

Thank you!

55

Additional resources

Jakub TurekPatterns & practices for unit-testing Swift-lyhttps://jakubturek.com/talks/

Jon ReidQuality Coding - Bloghttps://qualitycoding.org/blog/

Kasper B. GraversenGist: functional core, imperative shellhttps://tinyurl.com/y9cxblm8

56