nheagy
nheagy

Reputation: 363

How to TDD UIGestureRecognizers?

While trying to TDD code that would otherwise use a UIGestureRecognizer, I found no way to programatically verify the target-action. Without this, I'm not certain I can properly test it.

If the gesture recognizer is setup in IB (with iOS 5+ support) the target-action is setup when the NIB/Storyboard loads; if done in code it uses initWithTarget:action:, both of which mean that no amount of mocking would be able to detect the target-action.

I'm out of ideas. If anyone has successfully test-driven a UIGestureRecognizer I could use the advice.

Upvotes: 6

Views: 3441

Answers (4)

Evgenii
Evgenii

Reputation: 37339

This is how I unit test tap gesture recognizer in Swift. The test makes sure the code that responds to the tap gesture is executed.

First, I create an OnTap helper class

class OnTap: NSObject {
  var closure: ()->()

  init(view: UIView, gesture: UIGestureRecognizer, closure:() -> ()) {
    self.closure = closure
    super.init()
    view.userInteractionEnabled = true
    view.addGestureRecognizer(gesture)
    gesture.addTarget(self, action: "didTap:")
  }

  func didTap(gesture: UIGestureRecognizer) {
    closure()
  }
}

Next, I register a tap gesture wit a view and a callback closure

class MyClass {
  var onTap: OnTap?
  var didTap = false

  func setupTap() {
    let myView = UIView()
    onTap = OnTap(view: myView, gesture: UITapGestureRecognizer()) { [weak self] in
      self?.didTap = true
    }
  }
}

Finally, I simulate the tap in my unit test and check that the closure has been called

class MyGestureTests: XCTestCase {
  func testRepondToGesture() {
    let obj = MyClass()
    obj.setupTap()

    obj.onTap?.didTap(UITapGestureRecognizer())

    XCTAssert(obj.didTap)
  }
}

Note, this is a unit testing technique. In addition, I use UI tests to make sure all pieces work together in sweet harmony.

Upvotes: 3

Rudolf Adamkovič
Rudolf Adamkovič

Reputation: 31486

I'd suggest to subclass the gesture recognizer as follows:

class PanGestureRecognizer: UIPanGestureRecognizer {

    let initialTarget: Any?
    let initialAction: Selector?

    public override init(target: Any?, action: Selector?) {
        initialTarget = target
        initialAction = action
        super.init(target: target, action: action)
    }

}

Upvotes: 1

murat
murat

Reputation: 4963

If anyone is interested in a solution that uses private API, The following category on UIGestureRecognizer can be used to send gesture recognition actions. As Jonah points out, do not use it in production code.

@interface GGGestureRecognizerTarget : NSObject{
@public
  id _target;
  SEL _action;
}    
@end

@implementation GGGestureRecognizerTarget
@end

@interface UIGestureRecognizer (GGTest)     
- (void)sendActions;   
@end    

@implementation UIGestureRecognizer (GGTest)    
- (void)sendActions{
  id targets = [self valueForKey:@"_targets"];
  for (GGGestureRecognizerTarget* target in targets) {
    [self sendActionToTarget:target];
  }
}    
- (void)sendActionToTarget:(GGGestureRecognizerTarget*)recognizerTarget{
  id target = recognizerTarget->_target;
  SEL action = recognizerTarget->_action;
  [target performSelector:action withObject:self];
}    
@end

Upvotes: 0

Jonah
Jonah

Reputation: 17958

Unfortunately you're trying to inspect a framework class which wasn't written with testing in mind and so doesn't expose the state you want to verify. That is going to make it difficult to assert on the existence of the target-action pairs you want to check for. In such a case I have three options you might use, none of which are great solutions:

You might be able to subclass UIGestureRecognizer, override the target-action methods to save the registered pairs in a collection you can then expose to users of the class, and then call the superclass implementations of those methods. Unfortunately then you're introducing new classes just to make testing easier, have to remember to use them, and may have to cast from UIGestureRecognizer to your custom subclass depending on where you get a gesture recognizer reference from.

Alternately your test could swizzle new versions of the target-action methods into UIGestureRecognizer giving you a hook to track added targets. Just make sure to swap the original method implementations back into place when you're done or future tests will have unexpected behavior.

Finally you might be able to find a private API call which gives you a way to check the registered target-actions on the gesture recognizer. Just make sure that private API call remains only in your test code.

Upvotes: 4

Related Questions