问题
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