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:
button.sendActions(for: .touchUpInside)
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.
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? 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.
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.