Thursday, June 26, 2014

Unit Testing in Swift

Since Swift was released at the beginning of the month, I've been doing using it for most of my iOS development. It's been a pleasant experience: I've been able to discard huge amounts of boilerplate and take advantage of a few functional programming techniques that were previously unavailable on the iPhone and iPad.

One area where Swift has made huge improvements over Objective-C is unit tests. Objective-C's verbosity made it difficult to create small, focused classes to perform specific tasks. Plus, the language's insistence on keeping only one class to a file and the cumbersome pairing of every implementation file with a header imposed a hefty penalty on programmers who tried to divide their work up into discrete, testable components.

Unit testing in Swift is done with the same XCTest framework introduced back in Xcode 5 for Objective-C. But Swift's concision and its inclusion of modern language features like closures makes XCTest much more pleasant than it was to use under Objective-C. We'll walk through a very simple example of Swift unit testing below.

To get started, create an empty iOS Application project in Xcode called Counter. Xcode will generate a CounterTests folder for you and an associated test target.

First, let's create a simple class to be tested. Create the file "Counter.swift" and add the following code to it:

import Foundation

public class Counter {
  public var count: Int
  
  public init(count: Int) {
    self.count = count
  }
  
  public convenience init() {
    self.init(count: 0)
  }
  
  public func increment() {
    self.count++
  }

}

This is a very simple class, but it will be enough to illustrate how to use XCTest to test your own Swift code.

UPDATE: Note that as of Xcode 6.1, any symbols that you want to be visible in your test case should be declared public so they can be seen from the test target, which is distinct from your main application target. In the above example, the class above and any of its members that need to be accessed in the test case have been declared public. Thanks to Kaan Ersan for pointing this out in the comments.

Create a file called "CounterTest.swift" in the CounterTests folder Xcode generated for you (this simple test will be your "Hello, world" for Swift testing):

import XCTest
import Counter

class CounterTest: XCTestCase {
  func testSimpleAddition() {
    let counter = Counter()
    XCTAssertEqual(0, counter.count)
  }

}

NOTE: In the current version of Swift (Beta 2), you have to import your main target into the test target to get your tests to compile and run. This is why we import Counter at the top.

NOTE: I've seen a few Swift tutorials recommend that you use the built-in Swift function assert in your test cases - do not do this! assert will terminate your entire program if it fails. Using the XCTAssert functions provides a number of important benefits:

  • If one test case fails, your other cases can continue running; assert stops the entire program.
  • Because the XCTAssert functions are more explicit about what you're expecting, they can print helpful failure messages (e.g. "2 was not equal to 3") whereas assert can only report that its condition was false. There's a broad variety of assert functions, including XCTAssertLessThan, XCTAssertNil, etc.
  • The Swift language specification explicitly forbids string interpolation in the message passed to assert; the XCTAssert functions don't face this limitation.
To try your test code out, click "Test" on the "Product" menu. Your single test should pass.

We'll add two more test cases to create and exercise several instances of Counter and to ensure that the counter wraps around when it overflows:

import XCTest
import Test

class CounterTest: XCTestCase {
  func testInvariants() {
    let counter = Counter()
    XCTAssertEqual(0, counter.count, "Counter not initialized to 0")
    
    counter.increment()
    XCTAssertEqual(1, counter.count, "Increment is broken")

    XCTAssertEqual(1, counter.count, "Count has unwanted side effects!")
  }
  
  func testMultipleIncrements() {
    let counts = [1, 2, 3, 4, 5, 6]
    
    for count in counts {
      let counter = Counter()
      
      for i in 0..count {
        counter.increment()
      }
      
      XCTAssertEqual(counter.count, count, "Incremented value does not match expected")
    }
  }
  
  func testWraparound() {
    let counter = Counter(count: Int.max)
    counter.increment()
    
    XCTAssertEqual(counter.count, Int.min)
  }
}

These tests should pass as well.

You can find out more about XCTest in the Apple guide "Testing with Xcode." I hope this was helpful - please feel free to comment if anything is unclear.

4 comments:

  1. any issues with mixing in/ testing Objc classes in a Swift test case?

    ReplyDelete
  2. Thanks for the tutorial however, please change your Counter class to be public. Your CounterTest doesn't compile without Counter class being public. I was scratching my head trying to figure out what was wrong for sometime...

    ReplyDelete
    Replies
    1. Thanks for pointing this out, Kaan. I've updated the post.

      Delete