Slide 1

Slide 1 text

Protocol-Oriented Testing in Swift Carsten Könemann, Software Engineer @ hmmh 1

Slide 2

Slide 2 text

Hello, world! • Carsten Könemann • Software Engineer @ hmmh • iOS, Android Carsten Könemann, Software Engineer @ hmmh 2

Slide 3

Slide 3 text

Protocol-Oriented • Swift is a "protocol-oriented" programming language1 • Swift protocol == Java interface • but more powerful • Default implementations • Composable 1 Apple, WWDC 2015; https://developer.apple.com/videos/play/wwdc2015/408/. Carsten Könemann, Software Engineer @ hmmh 3

Slide 4

Slide 4 text

Testing Good tests are • Repeatable • Independent • Fast Carsten Könemann, Software Engineer @ hmmh 4

Slide 5

Slide 5 text

Testing Good tests are • Easy to write and execute • Input → Function to test → Check output • Avoid any logic Carsten Könemann, Software Engineer @ hmmh 5

Slide 6

Slide 6 text

Testing Carsten Könemann, Software Engineer @ hmmh 6

Slide 7

Slide 7 text

Mock Objects • Simulate input → deterministic • Decoupled from other code, database, network, etc → independent • No need to run on device / start UI → fast Carsten Könemann, Software Engineer @ hmmh 7

Slide 8

Slide 8 text

Example 1 Simple Mock Carsten Könemann, Software Engineer @ hmmh 8

Slide 9

Slide 9 text

Example 1 Simple Mock protocol Pet { func feed() } Carsten Könemann, Software Engineer @ hmmh 9

Slide 10

Slide 10 text

Example 1 Simple Mock class Cat: Pet { func feed() { print("miau") } } Carsten Könemann, Software Engineer @ hmmh 10

Slide 11

Slide 11 text

Example 1 Simple Mock class Fish: Pet { var mealsCount: Int = 0 func die() { print("blub") } func feed() { mealsCount += 1 if mealsCount > 9 { die() } } } Carsten Könemann, Software Engineer @ hmmh 11

Slide 12

Slide 12 text

Example 1 Simple Mock class Owner { var pet: Pet init(pet: Pet) { self.pet = pet } func beResponsible() { pet.feed() } } Carsten Könemann, Software Engineer @ hmmh 12

Slide 13

Slide 13 text

Example 1 Simple Mock class MockPet: Pet { var feedCallCount: Int = 0 func feed() { feedCallCount += 1 } } Carsten Könemann, Software Engineer @ hmmh 13

Slide 14

Slide 14 text

Example 1 Simple Mock class OwnerTests: XCTestCase { func testBeResponsible() { let pet = MockPet() let owner = Owner(pet: pet) owner.beResponsible() XCTAssert(pet.feedCallCount > 0) XCTAssert(pet.feedCallCount < 10) } } Carsten Könemann, Software Engineer @ hmmh 14

Slide 15

Slide 15 text

Example 1 Simple Mock extension MockPet { func validateFeedCallCount() { XCTAssert(feedCallCount > 0) XCTAssert(feedCallCount < 10) } } class OwnerTests: XCTestCase { func testBeResponsibleImproved() { let pet = MockPet() let owner = Owner(pet: pet) owner.beResponsible() pet.validateFeedCallCount() } } Carsten Könemann, Software Engineer @ hmmh 15

Slide 16

Slide 16 text

Example 2 CoreData Or: Why not just use subclasses? Carsten Könemann, Software Engineer @ hmmh 16

Slide 17

Slide 17 text

Example 2 CoreData @objc(MyCoreDataModel) public class MyCoreDataModel: NSManagedObject { @nonobjc public class func fetchRequest() -> NSFetchRequest { return NSFetchRequest(entityName: "MyCoreDataModel") } @NSManaged public var foobar: Bool } Carsten Könemann, Software Engineer @ hmmh 17

Slide 18

Slide 18 text

Example 2 CoreData class MyCoreDataController { static func doSomething(with model: MyCoreDataModel) -> String { if model.foobar == true { return "yay" } else { return "nay" } } } Carsten Könemann, Software Engineer @ hmmh 18

Slide 19

Slide 19 text

Example 2 CoreData class MyCoreDataControllerTests: XCTestCase { func testExample01() { let coreDataModel = MyCoreDataModel() coreDataModel.foobar = true XCTAssertEqual(MyCoreDataController.doSomething(with: coreDataModel), "yay") } } Carsten Könemann, Software Engineer @ hmmh 19

Slide 20

Slide 20 text

Example 2 CoreData class MyCoreDataControllerTests: XCTestCase { func testExample01() { let coreDataModel = MyCoreDataModel() coreDataModel.foobar = true XCTAssertEqual(MyCoreDataController.doSomething(with: coreDataModel), "yay") // failed: caught "NSInvalidArgumentException" // -[MyCoreDataModel setFoobar:]: unrecognized selector sent to instance } } Carsten Könemann, Software Engineer @ hmmh 20

Slide 21

Slide 21 text

Example 2 CoreData class MyCoreDataControllerTests: XCTestCase { func testExample01() { let coreDataModel = MyCoreDataModel() coreDataModel.foobar = true XCTAssertEqual(MyCoreDataController.doSomething(with: coreDataModel), "yay") // failed: caught "NSInvalidArgumentException" // -[MyCoreDataModel setFoobar:]: unrecognized selector sent to instance // CoreData Entities need be associated with an NSManagedObjectContext! } } Carsten Könemann, Software Engineer @ hmmh 21

Slide 22

Slide 22 text

Example 2 CoreData extension XCTestCase { func setUpInMemoryManagedObjectContext() -> NSManagedObjectContext { let managedObjectModel = NSManagedObjectModel.mergedModel(from: [Bundle.main])! let persistentStoreCoordinator = NSPersistentStoreCoordinator(managedObjectModel: managedObjectModel) do { try persistentStoreCoordinator.addPersistentStore(ofType: NSInMemoryStoreType, configurationName: nil, at: nil, options: nil) } catch { print("Adding in-memory persistent store failed") } let managedObjectContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) managedObjectContext.persistentStoreCoordinator = persistentStoreCoordinator return managedObjectContext } } Source: http://stackoverflow.com/a/39317100/1028701 Carsten Könemann, Software Engineer @ hmmh 22

Slide 23

Slide 23 text

Example 2 CoreData class MyCoreDataControllerTests: XCTestCase { var managedObjectContext: NSManagedObjectContext? override func setUp() { super.setUp() if managedObjectContext == nil { managedObjectContext = setUpInMemoryManagedObjectContext() } } func testExample02() { let coreDataModel = MyCoreDataModel(context: managedObjectContext!) coreDataModel.foobar = true XCTAssertEqual(MyCoreDataController.doSomething(with: coreDataModel), "yay") } } Carsten Könemann, Software Engineer @ hmmh 23

Slide 24

Slide 24 text

Example 2 CoreData Reminder: Good tests are • Deterministic • Independent • Fast • Easy to write Carsten Könemann, Software Engineer @ hmmh 24

Slide 25

Slide 25 text

Example 2 CoreData protocol MyCoreDataModelProtocol { var foobar: Bool { get } } extension MyCoreDataModel: MyCoreDataModelProtocol { // } Carsten Könemann, Software Engineer @ hmmh 25

Slide 26

Slide 26 text

Example 2 CoreData class MyCoreDataModelMock: MyCoreDataModelProtocol { var foobar: Bool } class MyCoreDataControllerTests: XCTestCase { func testExample03() { let coreDataModel = MyCoreDataModelMock() coreDataModel.foobar = true XCTAssertEqual(MyCoreDataController.doSomething(with: coreDataModel), "yay") } } Carsten Könemann, Software Engineer @ hmmh 26

Slide 27

Slide 27 text

Example 2 CoreData class MyCoreDataModelMock: MyCoreDataModelProtocol { // Use a stub for static test data var foobar: Bool { return true } } class MyCoreDataControllerTests: XCTestCase { func testExample03() { XCTAssertEqual(MyCoreDataController.doSomething(with: MyCoreDataModelMock()), "yay") } } Carsten Könemann, Software Engineer @ hmmh 27

Slide 28

Slide 28 text

Example 2 CoreData Seriously, why not just use subclasses? Carsten Könemann, Software Engineer @ hmmh 28

Slide 29

Slide 29 text

Example 2 CoreData class GeneratedCoreDataModel { ... } protocol CoreDataModelProtocol { ... } class CoreDataModel: GeneratedCoreDataModel, CoreDataModelProtocol { ... } class CoreDataMock: CoreDataModelProtocol { ... } Carsten Könemann, Software Engineer @ hmmh 29

Slide 30

Slide 30 text

Example 3 CoreLocation Or: Mocking other peoples code Carsten Könemann, Software Engineer @ hmmh 30

Slide 31

Slide 31 text

Example 3 CoreLocation class MyCoreLocationController: NSObject, CLLocationManagerDelegate { var locationManager: CLLocationManager? { didSet { locationManager?.delegate = self locationManager?.startMonitoring(for: CLCircularRegion(center: CLLocationCoordinate2D(latitude: 0, longitude: 0), radius: 5, identifier: "foo") ) } } var didVisitRegion = false func locationManager(_ manager: CLLocationManager, didEnterRegion region: CLRegion) { self.didVisitRegion = true } } Carsten Könemann, Software Engineer @ hmmh 31

Slide 32

Slide 32 text

Example 3 CoreLocation Traditionally tested: • Running around the office • Simulate location with Xcode Carsten Könemann, Software Engineer @ hmmh 32

Slide 33

Slide 33 text

Example 3 CoreLocation protocol CLLocationManagerProtocol { var delegate: CLLocationManagerDelegate? { get set } var monitoredRegions: Set { get } func startMonitoring(for region: CLRegion) } extension CLLocationManager: CLLocationManagerProtocol { // } Carsten Könemann, Software Engineer @ hmmh 33

Slide 34

Slide 34 text

Example 3 CoreLocation class CLLocationManagerMock: CLLocationManagerProtocol { var delegate: CLLocationManagerDelegate? var monitoredRegions: Set = Set() func startMonitoring(for region: CLRegion) { monitoredRegions.insert(region) } func simulateLocation(location: CLLocation) { for region in monitoredRegions { if let circularRegion = region as? CLCircularRegion, circularRegion.contains(location.coordinate) { delegate?.locationManager?(CLLocationManager(), didEnterRegion: region) } } } } Carsten Könemann, Software Engineer @ hmmh 34

Slide 35

Slide 35 text

Example 3 CoreLocation class MyCoreLocationControllerTests: XCTestCase { func testExample01() { let controller = MyCoreLocationController() let mockLocationManager = CLLocationManagerMock() controller.locationManager = mockLocationManager mockLocationManager.simulateLocation(location: CLLocation(latitude: 1, longitude: 1)) XCTAssertFalse(controller.didVisitRegion) mockLocationManager.simulateLocation(location: CLLocation(latitude: 0, longitude: 0)) XCTAssertTrue(controller.didVisitRegion) } } Carsten Könemann, Software Engineer @ hmmh 35

Slide 36

Slide 36 text

Thanks for your attention! Follow us on Twitter: @cargath @hmmh_de Carsten Könemann, Software Engineer @ hmmh 36