AppCode News Tips & Tricks

Tutorial: Unit testing in AppCode

In this tutorial, we will write simple unit tests in AppCode using different testing frameworks — XCTest and Quick/Nimble. You will learn how to create test classes and targets, run and debug tests, explore test coverage, and more.

Requirements:

  • AppCode 2019.3 or later
  • Xcode 11 or later

Initial project: iOSConferences
Final project: iOSConferences

Before you start

For this tutorial, we will use the iOSConferences application that we developed in the Use CocoaPods in your project tutorial. This application displays the up-to-date list of the upcoming conferences from the cocoaconferences.com website.

Download the iOSConferences project. Open the application in AppCode using the iOSConferences.xcworkspace file so that the pods already added to the project can be recognized by the IDE.

If CocoaPods gem is not installed yet, install it:

  • Click Tools | CocoaPods | Select Ruby SDK from the main menu.
  • In the Preferences dialog that opens, click Add Ruby SDK, and specify the path to the SDK (by default, /usr/bin/ruby).
  • Click Install CocoaPods.

In the run/debug configuration selector, select a device or simulator to run the application on and press ⌃R or click the Run button:

Run

The launched application should display the list of conferences:

Conference list

If you encounter problems with running the application, make sure that you are using AppCode 2019.3 or later (with SwiftUI supported) and CocoaPods were successfully installed on your side.

Step 1. Add a test target

When creating a new project, you can select the Include Unit Tests checkbox to have a test target and an XCTest class added. However, you can add it anytime while working with an existing project:

  • Press ⌘; to open the project settings.
  • Click the Add button, select iOS | Test | Unit Testing Bundle from the dialog that opens, and click Next:Add test target
  • On the next page, leave the default values in all the fields including the automatically generated target name iOSConferencesTests in the Product Name field and click Finish:Create test target

A new test target will be added to the project:

Test target added

This target contains a default XCTest class with stub code for several test methods:

Test stubs

Step 2. Create XCTest tests

Let’s create some tests to check if the conference date is displayed correctly on the details screen. Rename the iOSConferencesTests class to DateTests using the Rename refactoring ⇧F6:

Rename refactoring

Import the Yams library that is used for decoding the YAML file:

import Yams

Make the application code available for tests by adding the following line right after the import statements:

@testable import iOSConferences

Delete all existing stub code inside the DateTests class. In the DateTests class, add the decoder instance variable:

let decoder = YAMLDecoder()

Add the following test methods:

func testSameStartEndDatesShownCorrectly() {
    let yaml = try! decoder.decode([Conference].self, from:
    """
    - name: mDevCamp
      link: https://mdevcamp.eu/
      start: 2019-05-30
      end: 2019-05-30
      location:  Prague, Czech Republic
    """
    )
    let conference: Conference = yaml[0]
    let textDate = conference.textDates()
    XCTAssertEqual(textDate, "May 30, 2019")

}

func testDateWithoutEndShownCorrectly() {
    let yaml = try! decoder.decode([Conference].self, from:
    """
    - name: mDevCamp
      link: https://mdevcamp.eu/
      start: 2019-05-30
      location:  Prague, Czech Republic
    """
    )
    let conference: Conference = yaml[0]
    let textDate = conference.textDates()
    XCTAssertEqual(textDate, "May 30, 2019")
}

func testEndEarlierThanStartReplaced() {
    let yaml = try! decoder.decode([Conference].self, from:
    """
    - name: mDevCamp
      link: https://mdevcamp.eu/
      start: 2019-05-30
      end: 2019-05-29
      location:  Prague, Czech Republic
    """
    )
    let conference: Conference = yaml[0]
    let textDate = conference.textDates()
    XCTAssertEqual(textDate, "May 29, 2019 - May 30, 2019")
}

These methods test if the application shows the conference dates correctly in some specific cases:

  • testSameStartEndDatesShownCorrectly(): if the values in the start and end fields are the same, the application should display just one date instead of an interval (May 30, 2019 instead of May 30, 2019 - May 30, 2019).
  • testDateWithoutEndShownCorrectly(): if there is no end value specified for the conference, the application should display just the start date (May 30, 2019).
  • testEndEarlierThanStartReplaced(): if the start date is later than the end one, they should should be replaced (May 30, 2019 - May 31, 2019 instead of May 31, 2019 - May 30, 2019).

We will run and debug the created tests later. Now, let’s see how to create tests with the Quick and Nimble frameworks.

Step 3. Create Quick/Nimble tests

Let’s create some tests that will check if the application loads and parses the conferences data properly.

Install the Quick and Nimble frameworks

Open the existing Podfile by clicking Tools | CocoaPods | Edit Podfile in the main menu. Add the Quick and Nimble pods under the iOSConferencesTests target:

target 'iOSConferences' do
  use_frameworks!
  pod 'Yams'
  target 'iOSConferencesTests' do
    inherit! :search_paths
    pod 'Quick'
    pod 'Nimble'
  end
end

Click the Install pods link that appears in the top-right corner of the editor and wait until the two new pods are installed:

Install pods

Create a new test class

In the Project tool window ⌘1, right-click the iOSConferencesTests folder and select New | File from Xcode Template. In the dialog that opens, select iOS | Source | Unit Test Case Class and click Next.

On the next page, enter the filename — ApiTests, select Swift in the Language field, type QuickSpec in the Subclass of field, select iOSConferencesTests in the Targets list, and click Finish:

Create new test class

In the newly created Swift file, add the Yams, Quick, and Nimble import statements and delete all the default functions in the ApiTests class:

import XCTest
import Yams
import Quick
import Nimble

class ApiTests: QuickSpec {

}

Make the application code available for the tests by adding the following line right after the import statements:

@testable import iOSConferences

Create tests

Override the spec() method of the QuickSpec class: with the caret placed inside the ApiTests class, press ⌃O and select spec() in the dialog that opens:

Override spec method

Inside the spec() method, add the describe block and create the decoder and loader variables there:

override func spec() {
     describe("Application") {
         let decoder = YAMLDecoder()
         let loader = ConferencesLoader()
     }
}

Inside the describe block, add the following test methods in the it blocks:

it("should load conferences") {
    waitUntil(timeout: 5) { done in
        loader.loadConferences { conferences in
            done()
        }
    }
}

it("should parse conference") {
    let yaml = try! decoder.decode([Conference].self, from:
    """
    - name: mDevCamp
      link: https://mdevcamp.eu/
      start: 2019-05-30
      end: 2019-05-30
      location:  Prague, Czech Republic
    """
    )
    let conference: Conference = yaml[0]
    expect(conference.end).toNot(beNil())
    expect(conference.name).to(equal("mDevCamp"))
    expect(conference.start).toNot(beNil())
    expect(conference.location).to(equal(" Prague, Czech Republic"))
    expect(conference.link).to(equal("https://mdevcamp.eu/"))
}

it("should ignore unused fields") {
    let yaml = try! decoder.decode([Conference].self, from:
    """
    - name: mDevCamp
      link: https://mdevcamp.eu/
      start: 2019-05-30
      end: 2019-05-30
      location:  Prague, Czech Republic
      cocoa-only: true
      cfp:
        link: https://www.papercall.io/swift-to-2020
        deadline: 2020-06-16
    """
    )
    let conference: Conference = yaml[0]
    expect(conference.end).toNot(beNil())
    expect(conference.name).to(equal("mDevCamp"))
    expect(conference.start).toNot(beNil())
    expect(conference.location).to(equal(" Prague, Czech Republic"))
    expect(conference.link).to(equal("https://mdevcamp.eu/"))
}

These methods test the following:

  • it("should load conferences"): the network request performed in the loadConferences() method doesn’t return an empty response.
  • it("should parse conference"): the conference data is parsed correctly.
  • it("should ignore unused fields"): the conferences with fields not handled by the application (such as cocoa-only or cfp) are parsed correctly.

Step 4. Run and debug tests

In AppCode, you can quickly run and debug all methods in a test class as well as single methods (for XCTest) using the gutter icons in the editor the Run all/Run or the ⌃⇧R/⌃⇧D shortcuts. In this case, a temporary run/debug configuration is created which you can save and edit when needed.

For the XCTest framework, you can also run an arbitrary set of methods of one class by selecting and running ⌃⌘R them from the Run tool window or by creating a separate run/debug configuration where these methods are listed.

In this tutorial, we will create a run/debug configuration for running all the test classes available in the project, debug and fix failed tests, and check how much of the application code our tests cover.

Run all tests in project

To run all test classes in the test target, you need to create a special run/debug configuration. In the run/debug configuration selector, click Edit Configurations:

Edit configurations

In the dialog that opens, click Add and select XCTest:

Create test config

The XCTest run/debug configuration template is used for all XCTest-based frameworks, including Quick and Kiwi.

By default, AppCode creates a run/debug configuration for all classes in the test target:

All tests config

Save this configuration by clicking OK. Now it is pre-selected in the run/debug configuration selector:

Selected configuration

Press ⌃R to run all test classes. The Run tool window displays the results for all test classes available in the project, and you can see that there are two tests failed:

Run all test classes

To show/hide passed tests in the tree, click Checked on the toolbar.

Debug and fix the failed tests

Click the DateTests node in the tree. In the right-hand pane, you can see the stack trace with error messages helping you understand why the tests have failed. In our case, the expected values don’t match the actual ones returned by the textDate() method:

DateTests run results

Click the testEndEarlierThanStartReplaced test in the tree to navigate to its code in the editor.

If the navigation with a single click doesn’t work, double-click the test node or click Settings and select Navigate with Single Click.

You can also navigate to the methods’ code by clicking the links in the stack trace.

Set a breakpoint ⌘F8 at the following line:

Breakpoints in test

With the caret placed inside the testEndEarlierThanStartReplaced() method, press ⌃⇧D to run the test in debug mode. The program execution stops at the breakpoint and the Debug tool window opens.

For more information on how to examine a suspended program in debug mode, refer to Examine the suspended program.

Press F7 or click Step Into to go to the textDate() method implementation and step over its code lines pressing F8 or clicking Step Over. On the Variables tab of the Debug tool window as well as in the editor, you will see the value this method returns:

Debug method

As far as you can see, there’s no code for swapping the end and start dates in case they are mixed up in the source file. Replace the textDates() method’s code with the following:

func textDates() -> String {
    var result = start.dateToString()
    if let end = self.end {
        if start  end {
            result = "\(end.dateToString()) - \(result)"
        }
    }
    return result
}

In the Debug tool window, press Rerun failed tests button to rerun the failed tests and go to the Console tab:

Rerun failed tests

The execution is paused at the breakpoint at the moment. Press ⌥⌘R or click Resume to resume the program. All the tests are successfully passed now:

Successful test run

Run the tests with coverage

Finally, let’s check how much of the application code is covered by the unit tests:

  • Make sure the All Tests run/debug configuration is selected.
  • Click Run With Coverage button on the toolbar.

The Coverage tool window opens:

Code Coverage tool window

Double-click a folder to go to its contents or click the Level up button to navigate to the upper level. Moreover, you can see the percentage of files and lines covered by the tests in the Project tool window:

Code Coverage in the Project View

In the editor, the colored stripes appear in the gutter. Green means the line is fully covered by the tests, yellow — partially covered, red — uncovered:

Code Coverage indicators

You can set up other colors for coverage indication: in the Preferences ⌘, dialog, go to Preferences | Editor | Color Scheme | General and select Line Coverage from the list.

If you click the coverage indicator, the popup appears:

Code Coverage popup

Here you can see how many times the line was executed during tests and hide the coverage data by clicking Hide coverage. To show it again, press ⌥⌘F6 and select the necessary coverage suite from the dialog that opens.

For more information on test coverage in AppCode refer to Run with coverage.

Your AppCode team
JetBrains
The Drive to Develop