UI Test on UISegmented Control

68 Views Asked by At

GOAL: I want to UI test my SegmentedControl - check that each segment is selected when tapped on it

CONTEXT: The shoeType UISegmentedControl has 3 segments [City, Running, Baskets]

CODE: Here is my code:

ViewController.swift:

    @IBOutlet weak var shoeType: UISegmentedControl!

//    SegmentedControl pressed
    @IBAction func shoeTypePressed(_ sender: UISegmentedControl) {
        typeNumber = sender.selectedSegmentIndex // Assign the selected segment's index to typeNumber variable
        if typeNumber == 0 {
            type = "City"
        } else if typeNumber == 1 {
            type = "Running"
        } else {
            type = "Basket"
        }
        
        updateShoeImage() // Update the shoe image if type is pressed
        updateOrderResult() // Update order result's message when a type is pressed
    }

//    Updates the shoe image's shown
    func updateShoeImage() {
        shoeSelection.image = UIImage(named: getShoeParams(type: type, gender: genderLbl.text ?? "", color: color))
    }
    
//    Sets the value of the text displayed on the UI regarding the parameters
    func updateOrderResult() {
        if name == "" || name == "Mr." || name == "Miss" {
            if gender == "Boy" {
                name = "Mr."
            } else if gender == "Girl"{
                name = "Miss"
            }
        }
        orderResult.text = """
            Hello \(name) I found this pair of \(type.lowercased()) shoes in \(color.lowercased()) size \(sizeLbl.text?.lowercased() ?? "")
        """
    }

ViewControllerUITest.swift:

    func testSegmentedControl_WhenTapped_ChangeSegment() {
        app.launch()
        
        let shoeTypeSegmentedControl = app.segmentedControls["city"]
        app.segmentedControls["city"].tap()
        XCTAssertEqual(shoeTypeSegmentedControl.label, "City")
    }
1

There are 1 best solutions below

1
Jon Reid On

There is so much going in this code that it is hard to test with its current design. But let's focus only on this one requirement: Tapping a shoe type shows the name of the type in lowercase somewhere in the order result.

I would not reach for a UI test to do this. UI tests are slow, fragile, and costly to maintain. The reason I want tests is to support refactoring, so I want the tests to be quick and stable.

Following my book iOS Unit Testing by Example, these are the steps a unit test needs:

  • Arrange: Load the view controller
  • Act: Tap the segmented control
  • Assert: Check the output

To load the view controller (wherever it is in the storyboard), let's give it Storyboard ID. I like using the same name as the view controller.

func test_selectingShoeType_showsLowercaseTypeInOrder() throws {
    // Arrange:
    let storyboard = UIStoryboard(name: "Main", bundle: nil)
    let sut: ViewController = storyboard.instantiateViewController(identifier: "ViewController")
    sut.loadViewIfNeeded()
    let running = 1
    XCTAssertEqual(sut.shoeType.titleForSegment(at: running), "Running", "precondition")

    // Act:
    sut.shoeType.selectedSegmentIndex = running
    sut.shoeType.sendActions(for: .valueChanged)

    // Assert:
    let orderResult = sut.orderResult.text!
    XCTAssertTrue(
        orderResult.contains("running"),
        "Expected order containing 'running', but was \(orderResult)"
    )
}

On my machine, this test completes in 17ms. It's much faster than any UI test, and easily fast enough to support refactoring in small steps. (Remember, this is the reason I want tests.)

The assertion in the Arrange step is a precondition. Its job is to confirm that segment 1 is indeed "Running" which the rest of the test depends on.

If the order result doesn't contain what we expect, the assertion message reports the actual order result to make it easier to diagnose. I made the test fail on purpose to see how this message looks.

The test code itself is still hard to read. I would refactor it, moving some into setUp/tearDown, the precondition into its own test of the segment names, and some into helper methods:

func test_selectingShoeType_showsLowercaseTypeInOrder() throws {
    selectShoeType(running)
    assertOrderContains(shoeType: "running")
}

This hides the details, expressing the intent of the test in a way that anyone can read.