Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Protocol-Oriented Testing in Swift

Protocol-Oriented Testing in Swift

Carsten Könemann

May 12, 2017
Tweet

Other Decks in Programming

Transcript

  1. Hello, world! • Carsten Könemann • Software Engineer @ hmmh

    • iOS, Android Carsten Könemann, Software Engineer @ hmmh 2
  2. 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
  3. Testing Good tests are • Repeatable • Independent • Fast

    Carsten Könemann, Software Engineer @ hmmh 4
  4. 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
  5. 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
  6. Example 1 Simple Mock protocol Pet { func feed() }

    Carsten Könemann, Software Engineer @ hmmh 9
  7. Example 1 Simple Mock class Cat: Pet { func feed()

    { print("miau") } } Carsten Könemann, Software Engineer @ hmmh 10
  8. 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
  9. 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
  10. Example 1 Simple Mock class MockPet: Pet { var feedCallCount:

    Int = 0 func feed() { feedCallCount += 1 } } Carsten Könemann, Software Engineer @ hmmh 13
  11. 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
  12. 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
  13. Example 2 CoreData Or: Why not just use subclasses? Carsten

    Könemann, Software Engineer @ hmmh 16
  14. Example 2 CoreData @objc(MyCoreDataModel) public class MyCoreDataModel: NSManagedObject { @nonobjc

    public class func fetchRequest() -> NSFetchRequest<MyCoreDataModel> { return NSFetchRequest<MyCoreDataModel>(entityName: "MyCoreDataModel") } @NSManaged public var foobar: Bool } Carsten Könemann, Software Engineer @ hmmh 17
  15. 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
  16. 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
  17. 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
  18. 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
  19. 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
  20. 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
  21. Example 2 CoreData Reminder: Good tests are • Deterministic •

    Independent • Fast • Easy to write Carsten Könemann, Software Engineer @ hmmh 24
  22. Example 2 CoreData protocol MyCoreDataModelProtocol { var foobar: Bool {

    get } } extension MyCoreDataModel: MyCoreDataModelProtocol { // } Carsten Könemann, Software Engineer @ hmmh 25
  23. 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
  24. 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
  25. Example 2 CoreData class GeneratedCoreDataModel { ... } protocol CoreDataModelProtocol

    { ... } class CoreDataModel: GeneratedCoreDataModel, CoreDataModelProtocol { ... } class CoreDataMock: CoreDataModelProtocol { ... } Carsten Könemann, Software Engineer @ hmmh 29
  26. 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
  27. Example 3 CoreLocation Traditionally tested: • Running around the office

    • Simulate location with Xcode Carsten Könemann, Software Engineer @ hmmh 32
  28. Example 3 CoreLocation protocol CLLocationManagerProtocol { var delegate: CLLocationManagerDelegate? {

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

    var monitoredRegions: Set<CLRegion> = Set<CLRegion>() 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
  30. 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
  31. Thanks for your attention! Follow us on Twitter: @cargath @hmmh_de

    Carsten Könemann, Software Engineer @ hmmh 36