12/08/2018, 14:25

Unit Test in Swift

What is Unit Test? Unit test is awesome as it improves the overall quality of our code. A good unit test works on a particular function in isolation. For example, if there is a functionality which adds two numbers and gives total, then we must write unit test to check if the total result is ...

What is Unit Test?

Unit test is awesome as it improves the overall quality of our code. A good unit test works on a particular function in isolation. For example, if there is a functionality which adds two numbers and gives total, then we must write unit test to check if the total result is correct. Unit tests should be automated and it should run fast. That way it can easily track regressions, which means if the app has gone backwards in terms of functionality. So, if some code changes breaks any existing functionality unit test can be very effective in this case. Unit test can efficiently test output in terms of given test inputs.

Usability

It can test pretty much everything. For example, all the IBOutlets are properly hooked up in the Storyboard so that it does not unexpectedly crash when the ViewController shows up. Or Making sure a tableview has a cell registered for a nib file that is external to it, and that it won’t cause the app to crash when the tableview tries to get the cell. If any of those scenarios sound familiar, wouldn’t it be nice to have a way to know with near 100% certainty that there won’t be any problems before we run the app?

Adding Unit Test

For adding unit test in your project, first we have to open a new xcode project and check the box Include Unit Tests. This does all of the work of setting up unit tests, and a unit test target, for us.

Screen Shot 2016-12-26 at 9.40.25 AM.png

If we want to add manually we can go to File -> New -> Target -> IOS Unit Testing Bundle or we can do it by clicking "+" sign and select IOS Unit Testing Bundle like the screen shot given below:

Screen Shot 2016-12-26 at 9.41.52 AM.png

A Closer Look at the Functions

At the top of the test File created for example, in my case inside NewTests file we see

import XCTest
@testable import New

Screen Shot 2016-12-26 at 10.09.18 AM.png

All unit tests rely on Apple’s XCTest framework. Every test case we write will import it. It uses the @testable swift command to tell the compiler that we want to test against the "New" module.

Screen Shot 2016-12-26 at 10.09.18 AM.png

The class is called NewTests and it extends XCTestCase.There are 4 stub methods auto created, setUp() and tearDown() which run before and after every test, testExample(), which is a unit test stub and testPerformanceExample(), which we use when we want to know how long something takes to run. Note that all test methods in XCTest need to start with the word test. We get all this without having to write any code.

What are those diamond thingies? Try putting the mouse cursor over them. They are an inline button that runs your test. Apple has setup an example test. If we hover over the diamond next to it, we can click to run the test. Our app and its test target will get built, the test will run, and we will see the test succeeded notification. Cool! If we want to run all of the tests in this test case, click the button next to the class declaration.

While we’re here let’s add an assertion to the testExample() method. Change the testExample() to following code so we can see how the tests run in Xcode.

func testExample() {
        let result = 2 + 3
        XCTAssert(result == 4, "something gone wrong here")
    }

Now if we run the inline test we can see that it goves us an error-

Screen Shot 2016-12-26 at 10.33.15 AM.png

As the correct answer is suppose to be 5 but we tested against the condition result == 4, so the test failed. The string after the test is typically used to give us a hint on what test failed. But in this example, it’s just a simple “something gone wrong here” catch all error message.

Now If we change let result = 2 + 2 the test will pass as it should.

Screen Shot 2016-12-26 at 10.32.24 AM.png

Although a developer could get away with only using XCTAssert, the following helper assertions provide some useful semantics to help clarify what exactly is being tested. When possible, use the most specific assertion available, falling back to XCTAssert only in cases where it better expresses the intent.

  • Boolean Tests

For Bool values, or simple boolean expressions, use XCTAssertTrue & XCTAssertFalse:

XCTAssertTrue(expression, format...)

XCTAssertFalse(expression, format...)

P.S XCTAssert is equivalent to XCTAssertTrue.

  • Equality Tests

When testing whether two values are equal, use XCTAssert[Not]Equal for scalar values and XCTAssert[Not]EqualObjects for objects:

XCTAssertEqual(expression1, expression2, format...)

XCTAssertNotEqual(expression1, expression2, format...)

When specifically testing whether two Double, Float, or other floating-point values are equal, use XCTAssert[Not]EqualWithAccuracy, to account for any issues with floating point accuracy:

XCTAssertEqualWithAccuracy(expression1, expression2, accuracy, format...)

XCTAssertNotEqualWithAccuracy(expression1, expression2, accuracy, format...)

P.S XCTAssert[Not]EqualObjects is not necessary in Swift, since there is no distinction between scalars and objects.In addition to the aforementioned equality assertions, there are XCTAssertGreaterThan[OrEqual] & XCTAssertLessThan[OrEqual], which supplement == with >, >=, <, & <= equivalents for comparable values.

  • Nil Tests

Use XCTAssert[Not]Nil to assert the existence (or non-existence) of a given value:

XCTAssertNil(expression, format...)

XCTAssertNotNil(expression, format...)

  • Unconditional Failure

Finally, the XCTFail assertion will always fail:

XCTFail(format...)

XCTFail is most commonly used to denote a placeholder for a test that should be made to pass. It is also useful for handling error cases already accounted by other flow control structures, such as the else clause of an if statement testing for success.

  • Performance Testing

The measured block is executed ten times and the test output shows the average execution time as well as individual run times and standard deviation.

func testPerformanceExample() {
        let dateFormatter = NSDateFormatter()
        dateFormatter.dateStyle = .LongStyle
        dateFormatter.timeStyle = .ShortStyle

        let date = NSDate()

        measureBlock() {
            let string = dateFormatter.stringFromDate(date)
            print(string)
        }
    }

Screen Shot 2016-12-26 at 11.29.42 AM.png

Performance tests help establish a per-device baseline of performance for hot code paths and will fail if execution time becomes significantly slower. Sprinkle them into test cases to ensure that significant algorithms and procedures remain performant as time goes on.

  • XCTestExpectation

Say that we have a class that perform an asynchronous operation and executes a closure callback closure passed as a method parameter.

class SomeService {
  func doSomethingAsync(completion: (success: Bool) -> ()) { ...  }
}

let service = SomeService()
service.doSomethingAsync { success in
  // ...
}

Writing tests for code like doSomethingAsync in the traditional way will result in unexpected behaviours and false positives.

The reason this happens is because by default XCTest is synchronous, like most of the application code we usually write, while what we are trying to test is asynchronous. This means that the execution of the tests goes to the next line of code right after the async method is called, and the whole test finishes before the callback closure is run.

The XCTest framework provides us with an handy API to test the behaviour of asynchronous code: XCTestExpectation.Let's have a look at how to test doSomethingAsync using XCTestExpectation.

class CallbackTest: XCTestCase {

  func testAsyncCalback() {
    let service = SomeService()

    // 1. Define an expectation
    let expectation = expectationWithDescription("SomeService does stuff and runs the callback closure")

    // 2. Exercise the asynchronous code
    service.doSomethingAsync { success in
      XCTAssertTrue(success)

      // Don't forget to fulfill the expectation in the async callback
      expectation.fulfill()
    }

    // 3. Wait for the expectation to be fulfilled
    waitForExpectationsWithTimeout(1) { error in
      if let error = error {
        XCTFail("waitForExpectationsWithTimeout errored: (error)")
      }
    }
  }
}

As we can see there are three steps in the process.

  • Define an expectation with a meaningful description.
  • Go on with the test setup and exercise phases, calling the asynchronous method and fulfilling the expectation at the end of the callback closure.
  • Make the test runner wait for you expectation to be fulfilled, so that the asynchronous operations can be completed and you assertions verified.

It is important to provide a meaningful description because such description is reported in the failure message of an unfulfilled expectation:

error: -[PracticalTestingTests.CallbackTest testAsyncCalback] : Asynchronous wait failed: Exceeded timeout of 1 seconds, with unfulfilled expectations: "SomeService does stuff and succeeds".

When testing having descriptive failure messages is very important to make future self and the rest of the team identify the failure reason as soon as possible.

0