Feature Testing with KIF in Swift

One of the things I’m currently working on is getting back into my old Rails habit of testing my code. With KIF and XCTest frameworks, testing has become a lot simpler in iOS. However, as I started with KIF in my Swift app, I ran into an issue, which was luckily easy enough to resolve. But as I’ve been writing my tests in Swift, I’ve also found some new Swift-only patterns that I’ve been enjoying and wanted to share.

I’ll use my demo Seinfeld Quotes app as an example. I’m going to simply test the adding quotes functionality. Here is how the KIF test works:

Swift KIF Setup

To set up KIF, simply follow the instructions on the KIF README. Once you’ve successfully setup KIF, make sure to add it to the Bridging Header file in your KIF Test Target:

KIF Bridging Header

To trigger the creation of the Bridging Header file, I usually just create an Objective-C Test file in my target, which I then delete after the Bridging Header file is already created.

So you might be ready to start using KIF, but unfortunately the tester is defined in KIF using #define, which doesn’t work well with Swift.

While KIF is still under work for compatibility with Swift, I added an extension, as suggested by @bnickel in the Swift KIF Issue on Github, with a slight modification to keep the tester and system syntax a lot closer to the original:

//
//  KIF+SwiftExtension.swift
//  SeinfeldQuotes
//
//  Created by Natasha Murashev on 10/5/14.
//  Copyright (c) 2014 NatashaTheRobot. All rights reserved.
//

extension XCTestCase {
    
    var tester: KIFUITestActor { return tester() }
    var system: KIFSystemTestActor { return system() }
    
    private func tester(_ file : String = __FILE__, _ line : Int = __LINE__) -> KIFUITestActor {
        return KIFUITestActor(inFile: file, atLine: line, delegate: self)
    }
    
    private func system(_ file : String = __FILE__, _ line : Int = __LINE__) -> KIFSystemTestActor {
        return KIFSystemTestActor(inFile: file, atLine: line, delegate: self)
    }
}

Accessibility Labels

KIF uses Accessibility Labels to find views on each screen. Since Accessibility Labels can change easily (e.g. if the button title changes, for example), I like to keep the string versions of all my accessibility labels as constants.

However, with Swift, I found that it could be really nice to keep these together in enums in a private extension:

// AddQuoteTests.swift

class AddQuoteTests: KIFTestCase {
    
}

// MARK: Setup Accessibility Labels

private extension AddQuoteTests {
    enum ButtonLabel: String {
        case Add = "Add"
        case Save = "Save"
    }
    
    enum NavBarTitle: String {
        case CreateQuote = "Create Quote"
        case ListQuotes = "Seinfeld Quotes"
    }
    
    enum TextFieldLabel: String {
        case QuoteContent = "Quote Content"
        case SceneDescription = "Scene Description"
    }
    
    enum TextInput: String {
        case QuoteContent = "My Awesome Quote"
        case SceneDescription = "My Scene"
    }
    
}

The Test

I like to keep my tests very readable, so I’ve found myself keeping my test very simple on first glance:

//
//  AddQuoteTests.swift
//  SeinfeldQuotes
//
//  Created by Natasha Murashev on 10/5/14.
//  Copyright (c) 2014 NatashaTheRobot. All rights reserved.
//

class AddQuoteTests: KIFTestCase {
    
    func testAddQuote() {
        tapAddButton()
        fillInQuoteData()
        saveQuote()
        assertNewQuoteCreated()
    }
    
}

Meanwhile, I like to keep all the implementation details in another private extension:

class AddQuoteTests: KIFTestCase {
    
    func testAddQuote() {
        tapAddButton()
        fillInQuoteData()
        saveQuote()
        assertNewQuoteCreated()
    }
    
}

// MARK: Test Details

private extension AddQuoteTests {
    
    func tapAddButton() {
        tester.tapViewWithAccessibilityLabel(ButtonLabel.Add.toRaw(), traits: UIAccessibilityTraitButton)
        tester.waitForViewWithAccessibilityLabel(NavBarTitle.CreateQuote.toRaw(), traits: UIAccessibilityTraitStaticText)
    }
    
    func fillInQuoteData() {
        tester.enterText(TextInput.QuoteContent.toRaw(), intoViewWithAccessibilityLabel: TextFieldLabel.QuoteContent.toRaw())
        tester.enterText(TextInput.SceneDescription.toRaw(), intoViewWithAccessibilityLabel: TextFieldLabel.SceneDescription.toRaw())
    }
    
    func saveQuote() {
        tester.tapViewWithAccessibilityLabel(ButtonLabel.Save.toRaw(), traits: UIAccessibilityTraitButton)
        tester.waitForViewWithAccessibilityLabel(NavBarTitle.ListQuotes.toRaw(), traits: UIAccessibilityTraitStaticText)
    }
    
    func assertNewQuoteCreated() {
        tester.waitForViewWithAccessibilityLabel(TextInput.QuoteContent.toRaw(), traits: UIAccessibilityTraitStaticText)
        tester.waitForViewWithAccessibilityLabel(TextInput.SceneDescription.toRaw(), traits: UIAccessibilityTraitStaticText)
    }
}

I’ll definitely be working on adding more KIF tests to my future projects! Love how readable they can be with Swift.

Enjoy the article? Join over 14,500+ Swift developers and enthusiasts who get my weekly updates.

  • Nabeel

    Thanks for such a useful writeup and sample project. Can you also explain how you created your xcworkspace (SeinfeldQuotes project) with pods setup and KIF testing support? I’ve been trying to follow KIF’s readme but getting linking errors.

  • Thanks for introducing KIF. Looks great!

  • This is great, thank you!

  • Neeraj Kumar

    I am trying to setup testing in my new company’s iOS project. As I see KIF is great for UI testing but do we use for business logic tests or other tests like XCTest ? Should I rather start with Xcode XCtestCase or KIF?

    • Yes XCTest works for business logic, especially for your model unit tests. I also recommend taking a look at Quick, which is a lot like rspec in Rails – https://github.com/Quick/Quick

      • Neeraj Kumar

        But what about UI testing? KIF?

      • Danyal

        Did you try combining Quick with KIF? Would be nice to have scenarios driving the functional tests.

  • Maxime Capelle

    Thanks for the article, especially the tip about keeping the implementation in a private extension. It’s simple but very clever.

    You
    write at the end of your article that you would use KIF for your next
    projects. Do you have an opensource project on which you did use KIF for
    a complete bunch of tests ?
    I’m trying to find real uses of KIF because I’m facing some problems when I want to validate an app with KIF.

    For example, I have two labels “Firstname” and “Lastname” that must be updated with some values coming from somewhere.
    At the end of my test’s scenario I can only check that those values are present on the screen. (With waitForViewWithAccessibilityLabel)
    I don’t know how to check that the firstname value is now in the Firstname label and same thing for the lastname.

    If you have any pointer to this kind of cases, it would be very helpful !

    • Adam Tews

      Try grabbing the view using the UIView return type of waitForViewWithAccessibilityLabel, and then inspect the content of that view.

      For example:
      UIView *firstNameField = [tester waitForViewWithAccessibilityLabel:@”First Name”];

  • Angel Wu

    Natasha,

    I hope you are well 🙂 I’ve got some failure in the KIF tests, and the screenshot name is all the same (because we are using the __FILE__ here). This is what I did to get the screenshot file name to show the classname and the method name:

    func tester(file : String = “(self.dynamicType)”, _ line : Int = __LINE__) -> KIFUITestActor {
    let methodName: String = “(self.getInvocationSelector())”
    let fileName: String = “(self.dynamicType)”+ let someArray = fileName.componentsSeparatedByString(“.”)
    let fileToUse = someArray.last!
    return KIFUITestActor(inFile: “(fileToUse).(methodName)”, atLine: line, delegate: self)
    }

    The line number is still messed up, the the file name now shows the classname, followed by the method name.

    Might help to be able to see where things failed.

    Thank you.