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

Swift Testing Strategies

Swift Testing Strategies

Testing Swift code without the benefit of the Objective-C runtime presents some challenges if you're used to ObjC test frameworks like OCMock, Kiwi and Cedar. This presentation covers techniques you can use to achieve your stub and mock goals in pure Swift, and some things you should consider when designing code for testability.

Steve Madsen

July 10, 2015
Tweet

More Decks by Steve Madsen

Other Decks in Programming

Transcript

  1. Agenda • Open source alternatives to XCTest • How Swift

    differs from Obj-C under the hood • Techniques beyond simple asserts • Improving testability with code design changes
  2. XCTest xUnit style
 
 class MyTestClass: XCTestCase {
 override func

    setUp() { … }
 override func tearDown() { … }
 func testMath() {
 XCTAssertEqual(1 + 1, 2)
 }
 }
  3. Nimble Much more natural matchers
 
 class MyTestClass: XCTestCase {


    override func setUp() { … }
 override func tearDown() { … }
 func testMath {
 expect(1 + 1) == 2
 }
 } https://github.com/Quick/Nimble
  4. Quick RSpec-style
 
 class MyTestClass: QuickSpec {
 override func spec()

    {
 describe(“math”) {
 it(“works”) {
 expect(1 + 1) == 2
 }
 }
 }
 } https://github.com/Quick/Quick
  5. class MyTestClass: QuickSpec {
 override func spec() {
 describe(“MyClass”) {


    describe(“func1()”) {
 it(“does something”) { … }
 }
 }
 }
 }
  6. class MyTestClass: QuickSpec {
 override func spec() {
 describe(“MyClass”) {


    var instance: MyClass!
 
 beforeEach {
 instance = MyClass()
 }
 
 describe(“func1()”) {
 beforeEach {
 // more set-up of instance specific to func1()
 }
 
 it(“does something”) { … }
 }
 }
 }
 }
  7. class MyTestClass: QuickSpec {
 override func spec() {
 describe(“MyClass”) {


    var instance: MyClass!
 
 beforeEach {
 instance = MyClass()
 }
 
 describe(“func1()”) {
 beforeEach {
 // more set-up of instance specific to func1()
 }
 
 it(“does something”) { … }
 }
 }
 }
 }
  8. class MyTestClass: QuickSpec {
 override func spec() {
 describe(“MyClass”) {


    var instance: MyClass!
 
 beforeEach {
 instance = MyClass()
 }
 
 describe(“func1()”) {
 beforeEach {
 // more set-up of instance specific to func1()
 }
 
 it(“does something”) { … }
 }
 }
 }
 }
  9. Obj-C Method Dispatch [object doSomething] objc_msgSend(object, @selector(doSomething), nil) object->isa Selector

    IMP doThis 0x10a88a0 00 doThat 0x10a88a1 00 doNothing 0x10a88a2 30 doSomethi ng 0x10a88a3 00 … 0x10a88a5 a0 Class Method Table
  10. Obj-C Method Dispatch [object doSomething] objc_msgSend(object, @selector(doSomething), nil) object->isa Selector

    IMP doThis 0x10a88a0 00 doThat 0x10a88a1 00 doNothing 0x10a88a2 30 doSomethi ng 0x10a88a3 00 … 0x10a88a5 a0 Class Method Table
  11. Obj-C Method Dispatch [object doSomething] objc_msgSend(object, @selector(doSomething), nil) object->isa Selector

    IMP doThis 0x10a88a0 00 doThat 0x10a88a1 00 doNothing 0x10a88a2 30 doSomethi ng 0x10a88a3 00 … 0x10a88a5 a0 Class Method Table
  12. Obj-C Method Dispatch [object doSomething] objc_msgSend(object, @selector(doSomething), nil) object->isa JMP

    0x10a88a300 Selector IMP doThis 0x10a88a0 00 doThat 0x10a88a1 00 doNothing 0x10a88a2 30 doSomethi ng 0x10a88a3 00 … 0x10a88a5 a0 Class Method Table
  13. Swift Method Dispatch object.doSomething() Type Metadata Function Symbol Address +0x48

    _TFC4Test7MyClass6doThis 0x10a88a 000 +0x4b _TFC4Test7MyClass6doThat 0x10a88a 100 +0x50 _TFC4Test7MyClass9doNoth ing 0x10a88a 230 +0x54 _TFC4Test7MyClass11doSom ething 0x10a88a 300 +0x58 … 0x10a88a 5a0 vtabl callq *0x48(%rcx)
  14. Stubs • Isolates functionality external to code under test •

    ObjC: swap implementations at runtime • Swift: subclass within test file
  15. Stubs public class TemperatureSensor { public func readTemperature() -> Int

    { …
 } public var fahrenheit: Int { return readTemperature() * 1.8 + 32 } public var celsius: Int { return readTemperature() } }
  16. Stubs class TemperatureSensorTest: XCTestCase { class TestTemperatureSensor: TemperatureSensor { override

    func readTemperature() -> Int { return 10
 } } func testFahrenheit() { let sensor = TestTemperatureSensor() expect(sensor.fahrenheit) == 50 } }
  17. Stubs • Don’t stub your own networking calls • Look

    at Nocilla, ILTesting or OHHTTPStubs instead
  18. Mocks • A mock mirrors the interface to an external

    resource • Databases, key/value stores, system services, singletons • Not possible without dynamic dispatch
  19. Mocks What is easy in Obj-C (here, using Kiwi)…
 


    id mockSensor = [TemperatureSensor nullMock];
  20. Mocks Much less easy in Swift… public class MockTemperatureSensor: TemperatureSensor

    { override public func readTemperature() -> Int { return 0
 } override public var fahrenheit: Int { return 0 } override public var celsius: Int { return 0 } }
  21. public class Singleton { private static var instance: Singleton? public

    static func sharedInstance() -> Singleton { if self.instance == nil { self.instance = Singleton() } return self.instance! } private init() { …
 } }
  22. public class Singleton { private static var instance: Singleton? public

    static func sharedInstance() -> Singleton { if self.instance == nil { self.instance = Singleton() } return self.instance! } public static func setSharedInstance(instance: Singleton) { self.instance = instance } private init() { …
 } }
  23. public class Singleton { private static var instance: Singleton? public

    static func sharedInstance() -> Singleton { if self.instance == nil { self.instance = Singleton() } return self.instance! } static func setSharedInstance(instance: Singleton) { self.instance = instance } private init() { …
 } }
  24. Expectations • Similar to stubs, except a test fails if

    a function isn’t called the expected number of times • Used to verify usage of another class
  25. public class AuditLogger { public func log(…) { …
 }

    } public class UserManagement { public var logger: AuditLogger? public func grantAdminRights(…) { …
 }
 }
  26. class UserManagementTest: XCTestCase { class TestAuditLogger: AuditLogger { var logCalls

    = 0 override func log() { self.logCalls += 1 } } func testGrantAdminRightsCreatesAuditLogEntry() { let userManagement = UserManagement() let logger = TestAuditLogger() userManagement.logger = logger userManagement.grantAdminRights(…) expect(logger.logCalls) == 1 } }
  27. Dependency Injection • Instead of fetching an instance you need

    from outside your class, require that it be provided, either in init() or via a public property • Now it’s easy to supply a different instance, possibly of a subclass, during tests • Frees your class from assumptions that may not be valid in the future
  28. Factories • Create instances of an external resource on demand

    • DI is useful when the dependency is on a single instance • Factories are appropriate when you might need many instances
  29. Factories var factoryOverrides = [String:Any]() public class Factory<T> { public

    class func get(initializer: T.Type -> T) -> T { let theClass = (factoryOverrides["\(T.self)"]) ?? T.self return initializer(theClass as! T.Type) } public class func sub(newType: T.Type) { factoryOverrides["\(T.self)"] = newType } }
  30. Factories public class MyClass { var fileManagerFactory = { NSFileManager.defaultManager()

    } public func doesSomeWork() { if fileManagerFactory().fileExistsAtPath(…) { … } } }
  31. Factories @testable import MyClassModule class MyClassTests: XCTestCase { func testFileExists()

    { class TestFileManager: NSFileManager { override func fileExistsAtPath(path: String) -> Bool { return true } } let myClass = MyClass() myClass.fileManagerFactory = { TestFileManager() } … } }
  32. Nested Classes public class MyClassTests: XCTestCase { class Inner1 {}

    func testFunction() { class Inner2 {} let a = 1 var inner2 = Inner2() class Inner3 {} let inner3 = Inner3() } }