XCTestCase of a UIButton tap

浪子不回头ぞ 提交于 2020-01-06 04:54:06

问题


I am trying to unit test the wiring of button taps in a UIViewController but I'm finding these tests fail even though the code in the running app works fine.

I've simplified the failing test by removing the view controller and such leaving simply:

import XCTest

class ButtonTest: XCTestCase {

    var gotTap: XCTestExpectation!

    func test_givenButtonWithTargetForTapAction_whenButtonIsSentTapAction_thenTargetIsCalled() {

        gotTap = expectation(description: "Button tap recieved")

        let button = UIButton()
        button.addTarget(self, action: #selector(tap), for: .touchUpInside)

        button.sendActions(for: .touchUpInside)

        // Fails.
        wait(for: [gotTap], timeout: 0.1)
    }

    @objc func tap() {
        gotTap.fulfill()
    }
}

The the test does:

  • Wires up an action to a button that fulfils a test expectation
  • Taps the button using button.sendActions(for: .touchUpInside)
  • Waits for the expectations.

The failure is:

Asynchronous wait failed: Exceeded timeout of 0.1 seconds, with unfulfilled expectations: "Button tap recieved".

I don't want to use a UI test fo this. They are many orders of magnitude slower to execute, and a unit test should be ideal here.

Questions

  • Is this failing because the UIButton action sending requires some additional setup of the responder chain? Or a run loop that is not present here? Or something else? How can I set this up minimally in a unit test without instantiating a complete running app?
  • I initially tried to synchronously test button taps without an expectation (this also didn't work so I thought I'd try asynchronously) – if you can help to get this working, please also indicate if action sending is synchronous or asynchronous.

回答1:


A question about control events has an answer that control events require a UIApplication instance to send actions. Unfortunately the question doesn't indicate how this might be done.

There's also some code here that uses swizzling to patch the implementation of action sending on UIControl so that it doesn't delegate to UIApplication. It doesn't build with recent swift because it relies on overriding initialize – this might be fixable however.

The approach that I've taken is to implement an extension on UIControl for use in tests that provides a method simulateEvent(_ event: UIControl.Event) which goes like this:

extension UIControl {

    func simulateEvent(_ event: UIControl.Event) {
        for target in allTargets {
            let target = target as NSObjectProtocol
            for actionName in actions(forTarget: target, forControlEvent: event) ?? [] {
                let selector = Selector(actionName)
                target.perform(selector)
            }
        }
    }
}

This could be refined to properly examine the selector and determine if the sender and event should also be sent, but it's a reasonable proof of concept and allows for button sending in XCUnitTest. I also feel it's non invasive and accepts the fact that a unit test is not running in a full application environment, so tests can't make use of full responder handling.



来源:https://stackoverflow.com/questions/58519479/xctestcase-of-a-uibutton-tap

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!