Automated testing of the UI components has several challenges. The first challenge is to ensure that the UI tests are stable and do not fall from run to run for reasons independent of the code itself. For example, if a network request freezes, then the visual component will not be updated on time, a timeout will occur and the test will fail.

The second challenge is testability: to achieve isolation of the visual component from network services, hardware services (for example, geolocation), and also to ensure the ease of setting the component into the necessary state.

Let’s distinguish several types of tests:

  • system test, end-to-end test, e2e - testing the entire system as a whole;
  • unit test, component test - testing a single component.

In this article, under the test object, we consider the visual component that will be tested through the user interface. The subject of testing is the functional correctness of the test object. Thus, we consider a unit test of a visual component in isolation from its other dependencies.

Test object

Let’s consider the visual component “Suggest” (SearchVC), which implements the following use cases:

  1. The component shows the search results as you type a keyword.
  2. The user selects an item from the search results. 3.The user selects a keyword from the search bar.

From the architectural point of view, the SearchVC component has the following dependencies:

  • SearchVCDelegate
  • SuggestionService

Fig. 1. SearchVC class diagram.

The SearchVCDelegate interface contains only high-level events:

  1. Custom keyword select.
  2. Select a keyword from the suggested search results.

The SuggestionService interface contains a method that accepts the query parameter - a request for a hint, as well as completion - a function to which the service will transmit search results or an error.

Subject of testing

We may be interested in many aspects of the test object: functional correctness, performance, fault tolerance, security. The subject of testing is understood as the only aspect that interests us at the moment.

In this article, the subject of testing is the functional correctness of the component.

Solution

In the Xcode project, a separate target is created called UIDemo, in which we will test visual components in isolation. In this target, a typical master-detail application is created in which each element is a UIViewController under test.

For convenience and extensibility, a collection of viewControllers has been introduced, which contains pairs: the name of the component and the factory, which creates an instance of the component with all the necessary dependencies. Adding new visual components for testing is achieved by simply adding new pairs to the collection.

Listing 1. A list with all available visual components

class MasterViewController: UITableViewController {
    // Collection of UIViewControllers under test.
    // Implemented as pairs of titles and view-controller factories.
    var viewControllers : [(String, () -> UIViewController)] = [
        ("SearchVC", {
            let fakeSuggestService = FakeSuggestService(words: [
                "Apple",
                "Apricot",
                "Avocado",
                "Banana",
                "Blackberries",
                "Blackcurrant",
                "Blueberries",
                "Breadfruit"
            ])
            let searchViewController = DNTLSearchViewController()
            searchViewController.suggestionService = fakeSuggestService
            return searchViewController
        })
    ]

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return viewControllers.count
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
        cell.textLabel!.text = viewControllers[indexPath.row].0
        return cell
    }
    
    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let vcFactory = viewControllers[indexPath.row].1
        let vc = vcFactory()
        self.navigationController?.pushViewController(vc, animated: true)
    }
}

Fig. 2. UI with a list of all tested components

This approach allows you to run any visual component in one step with all the dependencies necessary for testing, which will be determined by the software engineer in the code. Thus, the problem of testability is solved when it is necessary to perform a complicated procedure for introducing the application into the tested state: clicking through the application to the visual component under test.

In addition, such target can serve as a good demonstration of all the available visual components in the application:

  • for manual testing;
  • to demonstrate the possibilities;
  • to find reusable components by other engineers.

Implement Test Doubles

We use the “Test Double” pattern to isolate the dependence on the SuggestionService. A test double [Meszaros, 2007] is such a component that is equivalent to a replaceable one, but suitable for testing.

Fig. 3. Class diagram of the component “Test double”

The function of the test double: it will take a list of words as a constructor parameter, and in the completion block it will pass only those that begin with the substring specified in query. Thus, we achieve controllability: in tests, we always know which result to expect from the test double.

class FakeSuggestService: SuggestService {
    
    var words: [String]
    
    init(words: [String]) {
        self.words = words
    }
    
    func suggest(query: String, completion: ([String], Error?) -> Void) {
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in
            completion(self!.words.filter { $0.starts(with: query) }, nil)
        }
    }
}

In the implementation of the suggest method, the test double calls the completion block asynchronously with a delay of 200 ms to simulate a delay in the response from the server. This will make the test double more adequate to reality.

Dependency Injection

The rigid dependence of one component on another is a typical sign of an untestable code, which leads to the rejection of automatic testing [Obrizan, 2019 (in Russian)]. In the case of visual components depending on network services, this leads to the fact that it is often difficult to predict the status of the server: what data is there, what will be the responses to test actions. In addition, the status of the network connection (delay, interruption) may affect the test results.

Reliable!

Program to an interface, not an implementation! (Erich Gamma)

Dependency injection [Fowler, 2004] — is a method of software design for testability, in which the dependence of one object on another can be replaced during compilation (build time) or application operation (run time).

There are four ways to inject dependencies:

  • through the class constructor;
  • through the property of the object;
  • through the method of the object;
  • through ServiceLocator.

In our solution, we took advantage of dependency injection through an object property.

Listing 3. Dependency injection through an object property.

let fakeSuggestService = FakeSuggestService(words: [
    "Apple",
    "Apricot",
    "Avocado",
    "Banana",
    "Blackberries",
    "Blackcurrant",
    "Blueberries",
    "Breadfruit"
])
let searchViewController = DNTLSearchViewController()
searchViewController.suggestionService = fakeSuggestService
return searchViewController

There are more advanced technologies for dependency injection. For example, Swinject [Swinject] is a technology that allows you to implement a dependency injection method in Swift projects.

Test plan

To drive test actions to the test object, the standard Apple iOS mechanism was chosen: the XCTest and XCUIApplication [Apple] classes.

Table 1. Test plan for the SearchVC component.

# Test Expectation
1 A Present: Apple, Apricot, Avocado
Absent: Banana
2 Ap Present: Apple, Apricot
Absent: Avocado
3 App Present: Apple
Absent: Apricot
4 Appo Present: Item coming soon! Tap to add.
Absent: Apple

We transfer the test plan to the automatic test code.

Listing 4. Auto test (fragment).

XCTAssertFalse(app!.staticTexts["Apple"].exists)

// Step 1.
app!.searchFields["Search categories"].typeText("A")
XCTAssertTrue(app!.staticTexts["Apple"].exists)
XCTAssertTrue(app!.staticTexts["Apricot"].exists)
XCTAssertTrue(app!.staticTexts["Avocado"].exists)
XCTAssertFalse(app!.staticTexts["Banana"].exists)

// Step 2.
app!.searchFields["Search categories"].typeText("p")
waitForAbsence(element: app!.staticTexts["Avocado"])
XCTAssertTrue(app!.staticTexts["Apple"].exists)
XCTAssertTrue(app!.staticTexts["Apricot"].exists)

// Step 3.
app!.searchFields["Search categories"].typeText("p")
XCTAssertTrue(app!.staticTexts["Apple"].exists)
waitForAbsence(element: app!.staticTexts["Apricot"])

// Step 4.
app!.searchFields["Search categories"].typeText("o")
waitForAbsence(element: app!.staticTexts["Apple"])
XCTAssertTrue(app!.tables.staticTexts["Item coming soon! Tap to add."].exists)

Fig. 4. Launch an automatic user interface test.

Conclusion

The article discusses the approach to unit testing of visual components, which eliminates the influence of dependencies (hardware, network and other services) on the test results.

Interface programming, dependency injection and test doubles significantly increase the testability of applications, reduce the time to develop and maintain tests, and achieve repeatability of test results.

References

  1. Meszaros, Gerard. XUnit test patterns : refactoring test code. : Pearson Education, Inc., 2007.

  2. Fowler, Martin. Inversion of Control Containers and the Dependency Injection pattern. — Martin Fowler, 2004 — Retrieved May 31, 2020.

  3. Gamma, Erich. Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley, 1995. 395 pages.

  4. Obrizan, Vladimir. Challenges in test automation. (in Russian) — Design and Test Lab, 2019 — Retrieved May 31, 2020.

  5. Obrizan, Vladimir. Notes on expert-lectures “Reliable software development”, 2018.

  6. Swinject. Dependency injection framework for Swift with iOS/macOS/Linux. — Retrieved June 21, 2020.

  7. User Interface Tests. Apple Developer Documentation. — Retrieved June 21, 2020.

About the author

Vladimir Obrizan
Vladimir Obrizan, PhD

Consultant to CEO and owners of software development companies. Managing director and и founder or First Institute of Reliable Software. Managing director и co-founder of DESIGN AND TEST LAB software development company. 15 years of experience as a software engineer, project manager, and top-management. 10 years of experience as a senior lecturer at Kharkov National University of Radio Electronics.