Making sure the UI updates as expected

We're going to write two tests to make sure that our trivia game works as expected. The first test will test that the question and answer buttons appear and that they have the correct labels. The second test will make sure that we can tap the answers and that the UI updates accordingly.

Instead of recording the tests, we're going to write them manually. Writing tests manually gives you a bit more control and allows you to do much more than just tapping on elements. Before we do this, you should open the Main.storyboard file and give accessibility identifiers to the UI elements. Select the question title and give the UILabel an identifier of QuestionTitle. Select each of the answers and give them the identifiers AnswerA, AnswerB, and AnswerC, respectively. Make sure your layout looks as shown in the following screenshot:

Remove the existing UI test from the MovieTriviaUITests class and add the one shown in the following code snippet:

func testQuestionAppears() {
    let app = XCUIApplication()
    
    let buttonIdentifiers = ["AnswerA", "AnswerB", "AnswerC"]
    for identifier in buttonIdentifiers {
        let button = app.buttons.matching(identifier: identifier).element
        let predicate = NSPredicate(format: "exists == true")
        _ = expectation(for: predicate, evaluatedWith: button, handler: nil)
    }
    
    let questionTitle = app.staticTexts.matching(identifier: "QuestionTitle").element
    let predicate = NSPredicate(format: "exists == true")
    _ = expectation(for: predicate, evaluatedWith: questionTitle, handler: nil)
    
    waitForExpectations(timeout: 5, handler: nil)
}

Each element is selected through its accessibility identifier. We can easily do this because the XCUIApplication instance we create provides easy access to our elements. Next, we create a predicate that will check whether the exists property of our element is true. Then we create an expectation that will evaluate the predicate for the UI element. Lastly, we wait for these expectations to fulfill. Our expectations are considered fulfilled whenever the predicate we pass to it is true. If this never happens, our tests fail and we'll know that something's wrong.

To make sure that our questions are loaded correctly, we'll need to load the JSON file like we did before. Add the following property to the test so we have a place to store the trivia questions:

var questions: [JSON]?

Next, add the following code to the setUp() method right after calling super.setUp():

guard let filename = Bundle(for: MovieTriviaUITests.self).path(forResource: "TriviaQuestions", ofType: "json"),
    let triviaString = try? String(contentsOfFile: filename),
    let triviaData = triviaString.data(using: .utf8),
    let jsonObject = try? JSONSerialization.jsonObject(with: triviaData, options: []),
    let triviaJSON = jsonObject as? JSON,
    let jsonQuestions = triviaJSON["questions"] as? [JSON]
    else { return }

questions = jsonQuestions
This code should look familiar to you because it's similar to the code we've already used to load json before. To make sure that the correct question is displayed, update the test method as shown below:
func testQuestionAppears() {
    // existing implementation...
    
    waitForExpectations(timeout: 5, handler: nil)
    
    guard let question = questions?.first
        else { fatalError("Can't continue testing without question data...") }
    
    validateQuestionIsDisplayed(question)
}

func validateQuestionIsDisplayed(_ question: JSON) {
    let app = XCUIApplication()
    let questionTitle = app.staticTexts.matching(identifier: "QuestionTitle").element
    
    guard let title = question["title"] as? String,
        let answerA = question["answer_a"] as? String,
        let answerB = question["answer_b"] as? String,
        let answerC = question["answer_c"] as? String
        else { fatalError("Can't continue testing without question data...") }
    
    XCTAssert(questionTitle.label == title, "Expected question title to match json data")
    
    let buttonA = app.buttons.matching(identifier: "AnswerA").element
    XCTAssert(buttonA.label == answerA, "Expected AnswerA title to match json data")
    
    let buttonB = app.buttons.matching(identifier: "AnswerB").element
    XCTAssert(buttonB.label == answerB, "Expected AnswerB title to match json data")
    
    let buttonC = app.buttons.matching(identifier: "AnswerC").element
    XCTAssert(buttonC.label == answerC, "Expected AnswerC title to match json data")
}

This code is run after we know for sure that our UI elements exist because it's executing after waiting for the expectations we created. The first question is extracted from the JSON data and all of the relevant labels are then compared to the question data using a method that we can reuse to validate that a certain question is currently shown.

The second test we should add is intended to check whether the game UI responds as expected. We'll load a question, tap the wrong answers, and make sure that the UI doesn't show the button to go to the next question. Then we'll tap the correct answer and tap the next question button. Finally, we'll validate that the second question is properly displayed and that the next question button is hidden again:

 func testAnswerValidation() {
    let app = XCUIApplication()
    
    let button = app.buttons.matching(identifier: "AnswerA").element
    let predicate = NSPredicate(format: "exists == true")
    _ = expectation(for: predicate, evaluatedWith: button, handler: nil)
    waitForExpectations(timeout: 5, handler: nil)
    
    let nextQuestionButton = app.buttons.matching(identifier: "NextQuestion").element
    
    guard let question = questions?.first,
        let correctAnswer = question["correct_answer"] as? Int
        else { fatalError("Can't continue testing without question data...") }
    
    let buttonIdentifiers = ["AnswerA", "AnswerB", "AnswerC"]
    for (i, identifier) in buttonIdentifiers.enumerated() {
        guard i != correctAnswer
            else { continue }
        
        app.buttons.matching(identifier: identifier).element.tap()
        
        XCTAssert(nextQuestionButton.exists == false, "Next question button should be hidden")
    }
    
    app.buttons.matching(identifier: buttonIdentifiers[correctAnswer]).element.tap()
    XCTAssert(nextQuestionButton.exists == true, "Next question button should be visible")
    
    nextQuestionButton.tap()
    
    guard let nextQuestion = questions?[1]
        else { fatalError("Can't continue testing without question data...") }
    
    validateQuestionIsDisplayed(nextQuestion)
    XCTAssert(nextQuestionButton.exists == false, "Next question button should be hidden")
}

The preceding code depicts the entire test that validates that our UI responds properly to good and bad answers. Tests like these are quite verbose, but they save you a lot of manual testing; tests like these are absolutely worth writing.

When you test your UI like this, you can rest assured that your app will at least be somewhat accessible. The beauty in this is that both UI testing and accessibility can greatly improve your app quality and each strongly aids the other.

Testing your UI is mostly a matter of looking for elements in the UI, checking their state or availability, and making assertions based on that. In the two tests we've written for MovieTrivia, we've combined expectations and assertions to test both existing UI elements and elements that might not be on screen yet. Note that your UI tests will always attempt to wait for any animations to complete before the next command is executed. This will make sure that you don't have to write expectations for any new UI that gets pushed using a transition.