iOS Unit Testing and UI Testing Tutorial

Testing, Testing …

What to Test?

Before writing any tests, it’s important to start with the basics: what do you need to test? If your goal is to extend an existing app, you should first write tests for any component you plan to change.

More generally, tests should cover:

  • Core functionality: model classes and methods, and their interactions with the controller
  • The most common UI workflows
  • Boundary conditions
  • Bug fixes

First Things FIRST: Best Practices for Testing

The acronym FIRST describes a concise set of criteria for effective unit tests. Those criteria are:

  • Fast: Tests should run quickly, so people won’t mind running them.
  • Independent/Isolated: Tests should not do setup or teardown for one another.
  • Repeatable: You should obtain the same results every time you run a test. External data providers and concurrency issues could cause intermittent failures.
  • Self-validating: Tests should be fully automated; the output should be either “pass” or “fail”, rather than a programmer’s interpretation of a log file.
  • Timely: Ideally, tests should be written just before you write the production code they test.

Following the FIRST principles will keep your tests clear and helpful, instead of turning into roadblocks for your app.

Getting Started

Download, unzip, open and inspect the starter projects BullsEye and HalfTunes.

BullsEye is based on a sample app in iOS Apprentice; I’ve extracted the game logic into a BullsEyeGame class and added an alternative game style.

In the lower-right corner there’s a segmented control to let the user select the game style: either Slide, to move the slider to get as close as possible to the target value, or Type, to guess where the slider position is. The control’s action also stores the user’s game style choice as a user default.

HalfTunes is the sample app from our NSURLSession Tutorial, updated to Swift 3. Users can query the iTunes API for songs, then download and play song snippets.

Let’s start testing!

Unit Testing in Xcode

Creating a Unit Test Target

The Xcode Test Navigator provides the easiest way to work with tests; you’ll use it to create test targets and run tests on your app.

Open the BullsEye project and hit Command-5 to open its test navigator.

Click the + button in the lower-left corner, then select New Unit Test Target… from the menu:

Accept the default name BullsEyeTests. When the test bundle appears in the test navigator, click it to open it in the editor. If BullsEyeTests doesn’t appear automatically, trouble-shoot by clicking one of the other navigators, then returning to the test navigator.

The template imports XCTest and defines a BullsEyeTests subclass of XCTestCase, with setup(), tearDown() and example test methods.

There are three ways to run the test class:

  1. Product\Test or Command-U. This actually runs all test classes.
  2. Click the arrow button in the test navigator.
  3. Click the diamond button in the gutter.

You can also run an individual test method by clicking its diamond, either in the test navigator or in the gutter.

Try the different ways to run the tests to get a feeling for how long it takes and what it looks like. The sample tests don’t do anything yet, so they’ll run really fast!

When all the tests succeed, the diamonds will turn green and show check marks. Click the gray diamond at the end of testPerformanceExample() to open the Performance Result:

You don’t need testPerformanceExample(), so delete it.

Using XCTAssert to Test Models

First, you’ll use XCTAssert to test a core function of BullsEye’s model: does a BullsEyeGame object correctly calculate the score for a round?

In BullsEyeTests.swift, add this line just below the import statement:

@testable import BullsEye

This gives the unit tests access to the classes and methods in BullsEye.

At the top of the BullsEyeTests class, add this property:

var gameUnderTest: BullsEyeGame!

Create and start a new BullsEyeGame object in setup(), after the call to super:

gameUnderTest = BullsEyeGame()
gameUnderTest.startNewGame()

This creates an SUT (System Under Test) object at the class level, so all the tests in this test class can access the SUT object’s properties and methods.

Here, you also call the game’s startNewGame method, which creates a targetValue. Many of your tests will use targetValue, to test that the game calculates the score correctly.

Before you forget, release your SUT object in tearDown(), before the call to super:

gameUnderTest = nil
Note: It’s good practice to create the SUT in setup() and release it in tearDown(), to ensure every test starts with a clean slate. For more discussion, check out Jon Reid’s post on the subject.

Now you’re ready to write your first test!

Replace testExample() with the following code:

// XCTAssert to test model
func testScoreIsComputed() {
  // 1. given
  let guess = gameUnderTest.targetValue + 5
  
  // 2. when
  _ = gameUnderTest.check(guess: guess)
  
  // 3. then
  XCTAssertEqual(gameUnderTest.scoreRound, 95, "Score computed from guess is wrong")
}

A test method’s name always begins with test, followed by a description of what it tests.

It’s good practice to format the test into given, when and then sections:

  1. In the given section, set up any values needed: in this example, you create a guess value so you can specify how much it differs from targetValue.
  2. In the when section, execute the code being tested: call gameUnderTest.check(_:).
  3. In the then section, assert the result you expect (in this case, gameUnderTest.scoreRound is 100 – 5) with a message that prints if the test fails.

Run the test by clicking the diamond icon in the gutter or in the test navigator. The app will build and run, and the diamond icon will change to a green checkmark!

Note: To see a full list of XCTestAssertions, Command-click XCTAssertEqual in the code to open XCTestAssertions.h, or go to Apple’s Assertions Listed by Category.
Note: The Given-When-Then structure of a test originated with Behavior Driven Development (BDD) as a client-friendly, low-jargon nomenclature. Alternative naming systems are Arrange-Act-Assert and Assemble-Activate-Assert.

Debugging a Test

There’s a bug built into BullsEyeGame on purpose, so now you’ll practice finding it. To see the bug in action, rename testScoreIsComputed to testScoreIsComputedWhenGuessGTTarget, then copy-paste-edit it to create testScoreIsComputedWhenGuessLTTarget.

In this test, subtract 5 from targetValue in the given section. Leave everything else the same:

func testScoreIsComputedWhenGuessLTTarget() {
  // 1. given
  let guess = gameUnderTest.targetValue - 5
  
  // 2. when
  _ = gameUnderTest.check(guess: guess)
  
  // 3. then
  XCTAssertEqual(gameUnderTest.scoreRound, 95, "Score computed from guess is wrong")
}

The difference between guess and targetValue is still 5, so the score should still be 95.

In the breakpoint navigator, add a Test Failure Breakpoint; this will stop the test run when a test method posts a failure assertion.

Run your test: it should stop at the XCTAssertEqual line with a Test Failure.

Inspect gameUnderTest and guess in the debug console:

guess is targetValue - 5 but scoreRound is 105, not 95!

To investigate further, use the normal debugging process: set a breakpoint at the when statement and also one in BullsEyeGame.swift, in check(_:), where it creates difference. Then run the test again, and step-over the let difference statement to inspect the value of difference in the app:

The problem is that difference is negative, so the score is 100 – (-5); the fix is to use the absolute value of difference. In check(_:), uncomment the correct line and delete the incorrect one.

Remove the two breakpoints and run the test again to confirm that it now succeeds.

Using XCTestExpectation to Test Asynchronous Operations

Now that you’ve learned how to test models and debug test failures, let’s move on to using XCTestExpectation to test network operations.

Open the HalfTunes project: it uses URLSession to query the iTunes API and download song samples. Suppose you want to modify it to use AlamoFire for network operations. To see if anything breaks, you should write tests for the network operations and run them before and after you change the code.

URLSession methods are asynchronous: they return right away, but don’t really finish running until some time later. To test asynchronous methods, you use XCTestExpectation to make your test wait for the asynchronous operation to complete.

Asynchronous tests are usually slow, so you should keep them separate from your faster unit tests.

Select New Unit Test Target… from the + menu and name it HalfTunesSlowTests. Import the HalfTunes app just below the import statement:

@testable import HalfTunes

The tests in this class will all use the default session to send requests to Apple’s servers, so declare a sessionUnderTest object, create it in setup() and release it in tearDown():

var sessionUnderTest: URLSession!

override func setUp() {
  super.setUp()
  sessionUnderTest = URLSession(configuration: URLSessionConfiguration.default)
}

override func tearDown() {
  sessionUnderTest = nil
  super.tearDown()
}

Replace testExample() with your asynchronous test:

// Asynchronous test: success fast, failure slow
func testValidCallToiTunesGetsHTTPStatusCode200() {
  // given
  let url = URL(string: "https://itunes.apple.com/search?media=music&entity=song&term=abba")
  // 1
  let promise = expectation(description: "Status code: 200")
  
  // when
  let dataTask = sessionUnderTest.dataTask(with: url!) { data, response, error in
    // then
    if let error = error {
      XCTFail("Error: \(error.localizedDescription)")
      return
    } else if let statusCode = (response as? HTTPURLResponse)?.statusCode {
      if statusCode == 200 {
        // 2
        promise.fulfill()
      } else {
        XCTFail("Status code: \(statusCode)")
      }
    }
  }
  dataTask.resume()
  // 3
  waitForExpectations(timeout: 5, handler: nil)
}

This test checks to see that sending a valid query to iTunes returns a 200 status code. Most of the code is the same as what you’d write in the app, with these additional lines:

  1. expectation(_:) returns an XCTestExpectation object, which you store in promise. Other commonly used names for this object are expectation and future. The description parameter describes what you expect to happen.
  2. To match the description, you call promise.fulfill() in the success condition closure of the asynchronous method’s completion handler.
  3. waitForExpectations(_:handler:) keeps the test running until all expectations are fulfilled, or the timeout interval ends, whichever happens first.

Run the test. If you’re connected to the internet, the test should take about a second to succeed after the app starts to load in the simulator.

Fail Faster

Failure hurts, but it doesn’t have to take forever. Here you’ll address how to quickly find out if your tests fail, saving time that could be better wasted on Facebook. :]

To modify your test so the asynchronous operation fails, simply delete the ‘s’ from “itunes” in the URL:

let url = URL(string: "https://itune.apple.com/search?media=music&entity=song&term=abba")

Run the test: it fails, but it takes the full timeout interval! This is because its expectation is that the request succeeded, and that’s where you called promise.fulfill(). Since the request fails, the test finishes only when the timeout expires.

You can make this test fail faster by changing its expectation: instead of waiting for the request to succeed, wait only until the asynchronous method’s completion handler is invoked. This happens as soon as the app receives a response — either OK or error — from the server, which fulfills the expectation. Your test can then check whether the request succeeded.

To see how this works, you’ll create a new test. First, fix this test by undoing the change to url, then add the following test to your class:

// Asynchronous test: faster fail
func testCallToiTunesCompletes() {
  // given
  let url = URL(string: "https://itune.apple.com/search?media=music&entity=song&term=abba")
  // 1
  let promise = expectation(description: "Completion handler invoked")
  var statusCode: Int?
  var responseError: Error?
  
  // when
  let dataTask = sessionUnderTest.dataTask(with: url!) { data, response, error in
    statusCode = (response as? HTTPURLResponse)?.statusCode
    responseError = error
    // 2
    promise.fulfill()
  }
  dataTask.resume()
  // 3
  waitForExpectations(timeout: 5, handler: nil)
  
  // then
  XCTAssertNil(responseError)
  XCTAssertEqual(statusCode, 200)
}

The key thing here is that simply entering the completion handler fulfills the expectation, and this takes about a second to happen. If the request fails, the then assertions fail.

Run the test: it should now take about a second to fail, and it fails because the request failed, not because the test run exceeded timeout.

Fix the url, then run the test again to confirm that it now succeeds.

Faking Objects and Interactions

Asynchronous tests give you confidence that your code generates correct input to an asynchronous API. You might also want to test that your code works correctly when it receives input from a URLSession, or that it correctly updates UserDefaults or a CloudKit database.

Most apps interact with system or library objects — objects you don’t control — and tests that interact with these objects can be slow and unrepeatable, violating two of the FIRST principles. Instead, you can fake the interactions by getting input from stubs or by updating mock objects.

Employ fakery when your code has a dependency on a system or library object — create a fake object to play that part and inject this fake into your code. Dependency Injection by Jon Reid describes several ways to do this.

Fake Input From Stub

In this test, you’ll check that the app’s updateSearchResults(_:) method correctly parses data downloaded by the session by checking that searchResults.count is correct. The SUT is the view controller, and you’ll fake the session with stubs and some pre-downloaded data.

Select New Unit Test Target… from the + menu and name it HalfTunesFakeTests. Import the HalfTunes app just below the import statement:

@testable import HalfTunes

Declare the SUT, create it in setup() and release it in tearDown():

var controllerUnderTest: SearchViewController!

override func setUp() {
  super.setUp()
  controllerUnderTest = UIStoryboard(name: "Main", 
      bundle: nil).instantiateInitialViewController() as! SearchViewController!
}

override func tearDown() {
  controllerUnderTest = nil
  super.tearDown()
}
Note: The SUT is the view controller because HalfTunes has a massive view controller problem — all the work is done in SearchViewController.swift. Moving the networking code into separate modules would reduce this problem, and also make testing easier.

Next, you’ll need some sample JSON data that your fake session will provide to your test. Just a few items will do, so to limit your download results in iTunes append &limit=3 to the URL string:

https://itunes.apple.com/search?media=music&entity=song&term=abba&limit=3

Copy this URL and paste it into a browser. This downloads a file named 1.txt or similar. Preview it to confirm it’s a JSON file, then rename it abbaData.json and add the file to the HalfTunesFakeTests group.

The HalfTunes project contains the supporting file DHURLSessionMock.swift. This defines a simple protocol named DHURLSession, with methods (stubs) to create a data task with either a URL or a URLRequest. It also defines URLSessionMock which conforms to this protocol, with initializers that let you create a mock URLSession object with your choice of data, response and error.

Set up the fake data and response, and create the fake session object, in setup() after the statement that creates the SUT:

let testBundle = Bundle(for: type(of: self))
let path = testBundle.path(forResource: "abbaData", ofType: "json")
let data = try? Data(contentsOf: URL(fileURLWithPath: path!), options: .alwaysMapped)

let url = URL(string: "https://itunes.apple.com/search?media=music&entity=song&term=abba")
let urlResponse = HTTPURLResponse(url: url!, statusCode: 200, httpVersion: nil, headerFields: nil)

let sessionMock = URLSessionMock(data: data, response: urlResponse, error: nil)

At the end of setup(), inject the fake session into the app as a property of the SUT:

controllerUnderTest.defaultSession = sessionMock
Note: You’ll use the fake session directly in your test, but this shows you how to inject it so that your future tests can call SUT methods that use the view controller’s defaultSession property.

Now you’re ready to write the test that checks whether calling updateSearchResults(_:) parses the fake data. Replace testExample() with the following:

// Fake URLSession with DHURLSession protocol and stubs
func test_UpdateSearchResults_ParsesData() {
  // given
  let promise = expectation(description: "Status code: 200")
  
  // when
  XCTAssertEqual(controllerUnderTest?.searchResults.count, 0, "searchResults should be empty before the data task runs")
  let url = URL(string: "https://itunes.apple.com/search?media=music&entity=song&term=abba")
  let dataTask = controllerUnderTest?.defaultSession.dataTask(with: url!) {
    data, response, error in
    // if HTTP request is successful, call updateSearchResults(_:) which parses the response data into Tracks
    if let error = error {
      print(error.localizedDescription)
    } else if let httpResponse = response as? HTTPURLResponse {
      if httpResponse.statusCode == 200 {
        promise.fulfill()
        self.controllerUnderTest?.updateSearchResults(data)
      }
    }
  }
  dataTask?.resume()
  waitForExpectations(timeout: 5, handler: nil)
  
  // then
  XCTAssertEqual(controllerUnderTest?.searchResults.count, 3, "Didn't parse 3 items from fake response")
}

You still have to write this as an asynchronous test because the stub is pretending to be an asynchronous method.

The when assertion is that searchResults is empty before the data task runs — this should be true, because you created a completely new SUT in setup().

The fake data contains the JSON for three Track objects, so the then assertion is that the view controller’s searchResults array contains three items.

Run the test. It should succeed pretty quickly, because there isn’t any real network connection!

Fake Update to Mock Object

The previous test used a stub to provide input from a fake object. Next, you’ll use a mock object to test that your code correctly updates UserDefaults.

Reopen the BullsEye project. The app has two game styles: the user either moves the slider to match the target value or guesses the target value from the slider position. A segmented control in the lower-right corner switches the game style and updates the gameStyle user default to match.

Your next test will check that the app correctly updates the gameStyle user default.

In the test navigator, click on New Unit Test Target… and name it BullsEyeMockTests. Add the following below the import statement:

@testable import BullsEye

class MockUserDefaults: UserDefaults {
  var gameStyleChanged = 0
  override func set(_ value: Int, forKey defaultName: String) {
    if defaultName == "gameStyle" {
      gameStyleChanged += 1
    }
  }
}

MockUserDefaults overrides the set(_:forKey:) method to increment the gameStyleChanged flag. Often you’ll see similar tests that set a Bool variable, but incrementing an Int gives you more flexibility — for example, your test could check that the method is called exactly once.

Declare the SUT and the mock object in BullsEyeMockTests:

var controllerUnderTest: ViewController!
var mockUserDefaults: MockUserDefaults!

In setup(), create the SUT and the mock object, then inject the mock object as a property of the SUT:

controllerUnderTest = UIStoryboard(name: "Main", bundle: nil).instantiateInitialViewController() as! ViewController!
mockUserDefaults = MockUserDefaults(suiteName: "testing")!
controllerUnderTest.defaults = mockUserDefaults

Release the SUT and the mock object in tearDown():

controllerUnderTest = nil
mockUserDefaults = nil

Replace testExample() with this:

// Mock to test interaction with UserDefaults
func testGameStyleCanBeChanged() {
  // given
  let segmentedControl = UISegmentedControl()
  
  // when
  XCTAssertEqual(mockUserDefaults.gameStyleChanged, 0, "gameStyleChanged should be 0 before sendActions")
  segmentedControl.addTarget(controllerUnderTest, 
      action: #selector(ViewController.chooseGameStyle(_:)), for: .valueChanged)
  segmentedControl.sendActions(for: .valueChanged)
  
  // then
  XCTAssertEqual(mockUserDefaults.gameStyleChanged, 1, "gameStyle user default wasn't changed")
}

The when assertion is that the gameStyleChanged flag is 0 before the test method “taps” the segmented control. So if the then assertion is also true, it means set(_:forKey:) was called exactly once.

Run the test; it should succeed.

UI Testing in Xcode

Xcode 7 introduced UI testing, which lets you create a UI test by recording interactions with the UI. UI testing works by finding an app’s UI objects with queries, synthesizing events, then sending them to those objects. The API enables you to examine a UI object’s properties and state in order to compare them against the expected state.

In the BullsEye project’s test navigator, add a new UI Test Target. Check that Target to be Tested is BullsEye, then accept the default name BullsEyeUITests.

Add this property at the top of the BullsEyeUITests class:

var app: XCUIApplication!

In setup(), replace the statement XCUIApplication().launch() with the following:

app = XCUIApplication()
app.launch()

Change the name of testExample() to testGameStyleSwitch().

Open a new line in testGameStyleSwitch() and click the red Record button at the bottom of the editor window:

When the app appears in the simulator, tap the Slide segment of the game style switch and the top label. Then click the Xcode Record button to stop the recording.

You now have the following three lines in testGameStyleSwitch():

let app = XCUIApplication()
app.buttons["Slide"].tap()
app.staticTexts["Get as close as you can to: "].tap()

If there are any other statements, delete them.

Line 1 duplicates the property you created in setup() and you don’t need to tap anything yet, so also delete the first line and the .tap() at the end of lines 2 and 3. Open the little menu next to ["Slide"] and select segmentedControls.buttons["Slide"].

So what you have is:

app.segmentedControls.buttons["Slide"]
app.staticTexts["Get as close as you can to: "]

Alter this to create a given section:

// given
let slideButton = app.segmentedControls.buttons["Slide"]
let typeButton = app.segmentedControls.buttons["Type"]
let slideLabel = app.staticTexts["Get as close as you can to: "]
let typeLabel = app.staticTexts["Guess where the slider is: "]

Now that you have names for the two buttons and the two possible top labels, add the following:

// then
if slideButton.isSelected {
  XCTAssertTrue(slideLabel.exists)
  XCTAssertFalse(typeLabel.exists)
  
  typeButton.tap()
  XCTAssertTrue(typeLabel.exists)
  XCTAssertFalse(slideLabel.exists)
} else if typeButton.isSelected {
  XCTAssertTrue(typeLabel.exists)
  XCTAssertFalse(slideLabel.exists)
  
  slideButton.tap()
  XCTAssertTrue(slideLabel.exists)
  XCTAssertFalse(typeLabel.exists)
}

This checks to see whether the correct label exists when each button is selected or tapped. Run the test — all the assertions should succeed.

Performance Testing

From Apple’s documentation: A performance test takes a block of code that you want to evaluate and runs it ten times, collecting the average execution time and the standard deviation for the runs. The averaging of these individual measurements form a value for the test run that can then be compared against a baseline to evaluate success or failure.

It’s very simple to write a performance test: you just put the code you want to measure into the closure of the measure() method.

To see this in action, reopen the HalfTunes project and, in HalfTunesFakeTests, replace testPerformanceExample() with the following test:

// Performance 
func test_StartDownload_Performance() {
  let track = Track(name: "Waterloo", artist: "ABBA", 
      previewUrl: "http://a821.phobos.apple.com/us/r30/Music/d7/ba/ce/mzm.vsyjlsff.aac.p.m4a")
  measure {
    self.controllerUnderTest?.startDownload(track)
  }
}

Run the test, then click the icon that appears next to the end of the measure() closure to see the statistics.

Click Set Baseline, then run the performance test again and view the result — it might be better or worse than the baseline. The Edit button lets you reset the baseline to this new result.

Baselines are stored per device configuration, so you can have the same test executing on several different devices, and have each maintain a different baseline dependent upon the specific configuration’s processor speed, memory, etc.

Anytime you make changes to an app that might impact the performance of the method being tested, run the performance test again to see how it compares to the baseline.

Code Coverage

The code coverage tool tells you what app code is actually being run by your tests, so you know what parts of the app code aren’t (yet) being tested.

Note: Should you run performance tests while code coverage is enabled? Apple’s documentation says: Code coverage data collection incurs a performance penalty … affect[ing] execution of the code in a linear fashion so performance results remain comparable from test run to test run when it is enabled. However, you should consider whether to have code coverage enabled when you are critically evaluating the performance of routines in your tests.

To enable code coverage, edit the scheme’s Test action and tick the Code Coverage box:

Run all your tests (Command-U), then open the reports navigator (Command-8). Select By Time, select the top item in that list, then select the Coverage tab:

Click the disclosure triangle to see the list of functions in SearchViewController.swift:

Mouse over the blue Coverage bar next to updateSearchResults(_:) to see that coverage is 71.88%.

Click the arrow button for this function to open the source file, then locate the function. As you mouse over the coverage annotations in the right sidebar, sections of code highlight green or red:

The coverage annotations show how many times a test hit each code section; sections that weren’t called are highlighted in red. As you’d expect, the for-loop ran 3 times, but nothing in the error paths was executed. To increase coverage of this function, you could duplicate abbaData.json, then edit it so it causes the different errors — for example, change "results" to "result" for a test that hits print("Results key not found in dictionary").

100% Coverage?

How hard should you strive for 100% code coverage? Google “100% unit test coverage”, and you’ll find a range of arguments for and against, along with debate over the very definition of “100% coverage”. Arguments-against say the last 10-15% isn’t worth the effort. Arguments-for say the last 10-15% is the most important, because it’s hard to test. Google “hard to unit test bad design” to find persuasive arguments that untestable code is a sign of deeper design problems. Further contemplation might lead to the conclusion that Test Driven Development is the way to go.

Advertisements
iOS Unit Testing and UI Testing Tutorial

Unit Test Private Methods

Unit testing a Swift project is quite different from unit testing a project written in Objective-C. If you’re used to the flexibility of the Objective-C runtime, then it may feel as if your hands are tied behind your back. Right?

Access Control

While access control is a very welcome addition with many benefits, it can complicate unit testing, especially if you’re new to unit testing. You probably know that you can apply the testable attribute to an import statement in a test target to gain access to entities that are declared internal.

Image

While this is a convenient addition, it doesn’t give you access to private entities in a test target. This brings us to the question of the day. How do you unit test private entities?

Wrong Question

The short answer to this question is simple. You cannot access private entities from another module and this also applies to test targets. Plain and simple. That’s what access control is for.

But that isn’t the answer to the question. If you ask how to unit test private entities, then you’re asking the wrong question. But why is that?

Mind Your Own Business

Why do you declare an entity private? What’s your motivation for doing so? Take a look at the following example.

Image

I’d like to unit test the AccountViewViewModel structure. As you can see, the AccountViewViewModel struct exposes two internal computed properties and it also defines a private method. The expiresAtAsString computed property offloads some of its work to the private parse(date:) method. Testing the internal computed properties is straightforward.

Image

But how do we test the private method? We cannot access the private method from the test target. But why should we unit test the private method? We marked it as private for a reason. Right? And that brings us to the answer to the question we started with. We don’t test private methods.

Unit Testing the Public Interface

By unit testing the public interface of the AccountViewViewModel struct we automatically or implicitly unit test the private interface of the struct. You have the task to make sure the public interface is thoroughly tested. This means that you need to make sure every code path of the AccountViewViewModel struct is covered by unit tests. In other words, the suite of unit tests should result in complete code coverage. That includes public, internal, and private entities.

If we enable code coverage in Xcode and we run the unit tests of the AccountViewViewModelstruct, we can see that some code paths are not executed.

Image

This tells us that the unit tests are incomplete. We can ignore the code path for the fatal error. I never unit test code paths that result in a fatal error, but that largely depends on how you use fatal errors in your projects.

We can increase code coverage for the AccountViewViewModel struct by adding one more unit test.

Image
Image

Implementation and Specification

It’s important to understand that we’re testing the specification of the AccountViewViewModelstruct. We’re not testing its implementation. While this may sound similar, it’s actually very different. We’re testing the functionality of the AccountViewViewModel struct. We’re not interested in how it does its magic under the hood.

The key takeaway is that private entities don’t need to be unit tested. Unit testing is a form of black-box testing. This means that we don’t test the implementation of the AccountViewViewModel struct, we test its specification.

This doesn’t mean that we’re not interested in the implementation, though. We need to make sure the suite of unit tests covers every code path of the entity we’re testing. Code coverage reports are invaluable to accomplish this.

Unit Test Private Methods

Write Mockup Classes For Unit Testing Of UserDefaults, Core Data & UrlSessions In Swift 3 & XCode 8

URLSession

Source Code

public protocol CLSession {
func dataTask(with url: URL, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask
func dataTask(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask
}

extension URLSession: CLSession { }

public final class URLSessionMock: CLSession {

var url: URL?
var request: URLRequest?
private let dataTaskMock: URLSessionDataTaskMock

public convenience init?(jsonDict: [String: Any], response: URLResponse? = nil, error: Error? = nil) {
guard let data = try? JSONSerialization.data(withJSONObject: jsonDict, options: []) else { return nil }
self.init(data: data, response: response, error: error)
}

public init(data: Data? = nil, response: URLResponse? = nil, error: Error? = nil) {
dataTaskMock = URLSessionDataTaskMock()
dataTaskMock.taskResponse = (data, response, error)
}

public func dataTask(with url: URL, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask {
self.url = url
self.dataTaskMock.completionHandler = completionHandler
return self.dataTaskMock
}

public func dataTask(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask {
self.request = request
self.dataTaskMock.completionHandler = completionHandler
return self.dataTaskMock
}

final private class URLSessionDataTaskMock : URLSessionDataTask {

typealias CompletionHandler = (Data?, URLResponse?, Error?) -> Void
var completionHandler: CompletionHandler?
var taskResponse: (Data?, URLResponse?, Error?)?

override func resume() {
DispatchQueue.main.async {
self.completionHandler?(self.taskResponse?.0, self.taskResponse?.1, self.taskResponse?.2)
}
}
}

}

Images
Use

UserDefaults

Source Code

class MockUserDefaults: UserDefaults {
var loggedInUser = 0
override func set(_ value: Any?, forKey defaultName: String) {
if defaultName == “loggedInUser” {
loggedInUser += 1
}
}
}

Images

Use

CoreData

Source Code

// MARK: – Variables
//Core Data variables
var storeCoordinator: NSPersistentStoreCoordinator!
var managedObjectContext: NSManagedObjectContext!
var managedObjectModel: NSManagedObjectModel!
var store: NSPersistentStore!

//Managers
var apiManager: APIManager!
var dataManager: DataManager!

// MARK: – XCTest Methods

override func setUp() {
super.setUp()
// Put setup code here. This method is called before the invocation of each test method in the class.
//Set Managers
apiManager = APIManager.shared
dataManager = DataManager.shared

/* Core Data Mock Object Configuration */
// ——– Start ——-
managedObjectModel = NSManagedObjectModel.mergedModel(from: nil)
storeCoordinator = NSPersistentStoreCoordinator(managedObjectModel: managedObjectModel)

do {
store = try storeCoordinator.addPersistentStore(ofType: NSInMemoryStoreType, configurationName: nil, at: nil, options: nil)
} catch {
print(error.localizedDescription)
}

managedObjectContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
managedObjectContext.persistentStoreCoordinator = storeCoordinator
apiManager.context = managedObjectContext
dataManager.context = managedObjectContext
(UIApplication.shared.delegate as! AppDelegate).mockContext = managedObjectContext
// ——– End ——-
}

override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
managedObjectContext = nil
apiManager = nil
dataManager = nil
(UIApplication.shared.delegate as! AppDelegate).mockContext = nil

//Check Store Removal
do {
try storeCoordinator.remove(store)
} catch {
XCTFail(“couldn’t remove persistant store: \(error)”)
}

super.tearDown()
}

func testThatStoreIsSetUp() {
XCTAssertNotNil(store, “no persitant store”)
}

Images

Use

Write Mockup Classes For Unit Testing Of UserDefaults, Core Data & UrlSessions In Swift 3 & XCode 8