You're going to write two tests to make sure that the 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 the answers can be tapped and that the UI updates accordingly.
Instead of recording the tests, you'll 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 you do this, you should open the Main.storyboard file and give accessibility identifiers to the UI elements. Select the question title and give UILabel an identifier of QuestionTitle. Select each of the answers and give them the AnswerA, AnswerB, and AnswerC identifiers, respectively. Also, give the next button an accessibility identifier of NextQuestion. The following screenshot shows what the question title should look like:
Remove the existing UI test, called testExample(), from the MovieTriviaUITests class and add the one shown in the following code snippet:
func testQuestionAppears() { let app = XCUIApplication() // 1 let buttonIdentifiers = ["AnswerA", "AnswerB", "AnswerC"] for identifier in buttonIdentifiers { let button = app.buttons.matching(identifier: identifier).element // 2 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) // 3 waitForExpectations(timeout: 5, handler: nil) }
Each element is selected through its accessibility identifier. You can do this because the XCUIApplication instance we create provides easy access to the UI elements. Next, a predicate is created that is used to check whether each element exists and an expectation is created. This expectation will continuously evaluate whether the predicate is true and once it is, the predicate will be fulfilled automatically. Lastly, the UI test will wait for all expectations to be fulfilled.
To make sure the questions are loaded correctly, you should load the JSON file as you did before. Add the following property to the test so you have a place to store the trivia questions:
typealias JSON = [String: Any] var questions: [JSON]?
Next, add the following code to the setUp() method right after calling super.setUp() and before launching the app:
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 }
This code should look familiar to you because it's similar to the code you already used to load JSON. To make sure that the correct question is displayed, update the test method as shown here:
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) }
The preceding code calls validateQuestionIsDisplayed(_:), but this method is not implemented yet. Add the following implementation:
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 checking that the UI elements exist because it's run 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 reusable method that validates whether a specific question is currently shown.
The second test you should add is intended to check whether the game UI responds as expected. After loading a question, the test will tap on the wrong answers and then makes sure the UI doesn't show the button to go to the next question. Then, the correct answer will be selected, and the test will attempt to navigate to the next question. And of course, the test will then validate that the next question is shown:
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 shows the entire test that validates that the UI responds appropriately to correct and incorrect answers. Tests such as these are quite verbose, but they save you a lot of manual testing.
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 significantly improve your app quality and each actively 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 you 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 asyncronous expectations for any new UI that is added to the screen with an animation.