AppCode 2020.2 Help

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, view the test coverage information, and more.

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.

  1. Download the iOSConferences project.

  2. 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.

  3. 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.

  4. In the run/debug configuration selector, select a device or simulator to run the application on and press ⇧F10 or click the Run button:

    Select device

    The launched application should display the list of conferences:

    The iOSConferences application

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.

  1. Press ⌃⌥⇧S to open the project settings.

  2. Click the Add button, select iOS | Test | Unit Testing Bundle from the dialog that opens, and click Next:

    Add a test target

  3. 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:

    Add a test target

A new test target will be added to the project.

Added test target

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

Stub code for an XCTest class

Step 2. Create XCTest tests

Let's create some tests to check if the conference date is displayed correctly on the details screen.

  1. Rename the iOSConferencesTests class to DateTests using the Rename refactoring ⇧F6:

    Rename test

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

    import Yams

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

    @testable import iOSConferences

  4. Delete all existing stub code inside the DateTests class.

  5. In the DateTests class, add the decoder instance variable:

    let decoder = YAMLDecoder()

  6. 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 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.

1. Install the Quick and Nimble frameworks

  1. Open the existing Podfile by clicking Tools | CocoaPods | Edit Podfile in the main menu.

  2. 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

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

    The Install pods link

2. Create a new test class

  1. In the Project tool window ⌥1, right-click the iOSConferencesTests folder and select New | File from Xcode Template.

  2. In the dialog that opens, select iOS | Source | Unit Test Case Class and click Next.

  3. On the next page, specify the following:
    • File name: ApiTests

    • Language: Swift

    • Subclass of: QuickSpec

    • Targets: iOSConferencesTests

    New test class
  4. In the newly created Swift file, add the Yams, Quick, and Nimble import statements and delete all the default methods in the ApiTests class:

    import XCTest import Yams import Quick import Nimble class ApiTests: QuickSpec { }

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

    @testable import iOSConferences

3. Create tests

  1. 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 methods

  2. 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() } }

  3. 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 button/the Run button or the ⌃⇧F10/⌃⇧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. See more in Create run/debug configurations for tests.

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.

1. Run all tests in the project

To run all test classes in the test target, you need to create a special run/debug configuration.

  1. In the run/debug configuration selector, click Edit Configurations:

    Create a test run/debug configuration

  2. In the dialog that opens, click the Add button and select XCTest:
    Add a new XCTest configuration
  3. By default, AppCode creates a run/debug configuration for all classes in the test target:

    All tests configurations

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

    All Test configuration selected
  4. Press ⇧F10 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:
    All tests passed

2. Debug and fix the failed tests

  1. 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:

    The test log

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

  3. Set a breakpoint ⌃F8 at the following line:

    The breakpoint in the test method

  4. 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.
  5. Press F7 or click the Step Into button to go to the textDate() method implementation and step over its code lines pressing F8 or clicking Step Over button. On the Variables tab of the Debug tool window as well as in the editor, you will see the value this method returns:
    Debugging 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 = "\(result) - \(end.dateToString())" } else if start > end { result = "\(end.dateToString()) - \(result)" } } return result }
  6. In the Debug tool window, press the Rerun Failed Tests button to rerun the failed tests and go to the Console tab:

    Paused test

    The execution is paused at the breakpoint at the moment. Press F9 or click icons.actions.resume.svg to resume the program. All the tests are successfully passed now:

    All tests fixed

3. Run the tests with coverage

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

  1. Make sure the All Tests run/debug configuration is selected.

  2. Click the Run with Coverage button on the toolbar.

The Coverage tool window opens:

The Coverage tool window

Double-click a folder to go to its contents or click the the Go Up button 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:

Coverage in the Project tool window

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:

Coverage indicators in the editor

If you click the coverage indicator, the popup appears:

Coverage indicators

Here you can see how many times the line was executed during testing 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 Running with coverage.

Last modified: 10 September 2020