21

編集:質問をより明確にするために更新

編集 2: 実世界の問題に対して質問をより正確にしました。画面上のテキストフィールド以外の場所をタップした場合、私は実際に行動を起こしたいと考えています. したがって、テキストフィールド内のイベントを単純にリッスンすることはできません。イベントがビュー内のどこかでタップされたかどうかを知る必要があります。

ビューの特定の座標内でジェスチャ認識エンジンがタップを認識したときに特定のアクションが実行されることをアサートする単体テストを作成しています。UITapGestureRecognizer によって処理される (特定の座標で) タッチをプログラムで作成できるかどうかを知りたいです。 単体テスト中のユーザー操作をシミュレートしようとしています。

UITapGestureRecognizer は Interface Builder で構成されます

//MYUIViewControllerSubclass.m

-(IBAction)viewTapped:(UITapGestureRecognizer*)gesture {
  CGPoint tapPoint = [gesture locationInView:self.view];
  if (!CGRectContainsPoint(self.textField, tapPoint)) {
    // Do stuff if they tapped anywhere outside the text field
  }
}

//MYUIViewControllerSubclassTests.m
//What I'm trying to accomplish in my unit test:

-(void)testThatTappingInNoteworthyAreaTriggersStuff {
  // Create fake gesture recognizer and ViewController
  MYUIViewControllerSubclass *vc = [[MYUIViewControllersSubclass alloc] init];
  UITapGestureRecognizer *tgr = [[UITapGestureRecognizer initWithView: vc.view];

  // What I want to do:
  [[ Simulate A Tap anywhere outside vc.textField ]]
  [[  Assert that "Stuff" occured ]]
}
4

8 に答える 8

9

ここには複数のオプションがあると思います:

  1. ビューに a を送信するのが最も簡単push event actionかもしれませんが、タップアクションが発生する場所を選択できるようにしたいので、本当に欲しいとは思いません。

    [yourView sendActionsForControlEvents: UIControlEventTouchUpInside];

  2. UI automation toolXCodeインストゥルメントで提供されているものを使用できます。このブログでは、スクリプトを使用して UI テストを自動化する方法について詳しく説明しています。

  3. iPhoneでタッチイベントを合成する方法を説明するこのソリューションもありますが、それらは単体テストにのみ使用してください。これはハックのように聞こえますが、前の 2 つのポイントでニーズが満たされない場合の最後の手段として、このソリューションを検討します。

于 2012-12-30T22:44:26.237 に答える
4

(iTunes-)合法的な道を歩みながら、あなたがやろうとしていることは非常に難しい(しかし完全に不可能ではない).


最初に正しい方法で下書きをさせてください。

これを行うための適切な方法は、UIAutomation を使用することです。UIAutomation はまさにあなたが求めていることを行い、あらゆる種類のテストでユーザーの行動をシミュレートします。


さて、その難しい道。

問題が発生する問題は、新しい UIEvent をインスタンス化することです。(残念ながら) 残念ながら、UIKit は、明らかなセキュリティ上の理由から、そのようなイベントのコンストラクターを提供していません。ただし、過去に機能した回避策がありますが、現在も機能しているかどうかは不明です。

Matt Galagher のすばらしいブログで、タッチ イベントを合成する方法に関するソリューションのドラフトをご覧ください。

于 2012-12-30T22:58:36.380 に答える
2
CGPoint tapPoint = [gesture locationInView:self.view];

する必要があります

CGPoint tapPoint = [gesture locationInView:gesture.view];

cgpoint は、ビュー内のどこにあるかを推測しようとするのではなく、ジェスチャ ターゲットがある正確な場所から取得する必要があるためです。

于 2013-11-21T23:00:52.273 に答える
2

さて、私は上記を機能するカテゴリに変えました。

興味深いビット:

  • カテゴリはメンバー変数を追加できません。追加したものはすべてクラスに対して静的になるため、Apple の多くの UITapGestureRecognizer によって上書きされます。
    • したがって、associated_object を使用して魔法を実現してください。
    • 非オブジェクトを格納するための NSValue
  • Apple のinit方法には、重要な構成ロジックが含まれています。何が設定されているかを推測できます (タップ数、タッチ数、他には?
    • しかし、これは運命です。そのため、モックを保持する init メソッドをスウィズルします。

ヘッダー ファイルは簡単です。これが実装です。

#import "UITapGestureRecognizer+Spec.h"
#import "objc/runtime.h"

/*
 * With great contributions from Matt Gallagher (http://www.cocoawithlove.com/2008/10/synthesizing-touch-event-on-iphone.html)
 * And Glauco Aquino (http://stackoverflow.com/users/2276639/glauco-aquino)
 * And Codeshaker (http://codeshaker.blogspot.com/2012/01/calling-original-overridden-method-from.html)
 */
@interface UITapGestureRecognizer (SpecPrivate)

@property (strong, nonatomic, readwrite) UIView *mockTappedView_;
@property (assign, nonatomic, readwrite) CGPoint mockTappedPoint_;
@property (strong, nonatomic, readwrite) id mockTarget_;
@property (assign, nonatomic, readwrite) SEL mockAction_;

@end

NSString const *MockTappedViewKey = @"MockTappedViewKey";
NSString const *MockTappedPointKey = @"MockTappedPointKey";
NSString const *MockTargetKey = @"MockTargetKey";
NSString const *MockActionKey = @"MockActionKey";

@implementation UITapGestureRecognizer (Spec)

// It is necessary to call the original init method; super does not set appropriate variables.
// (eg, number of taps, number of touches, gods know what else)
// Swizzle our own method into its place. Note that Apple misspells 'swizzle' as 'exchangeImplementation'.
+(void)load {
    method_exchangeImplementations(class_getInstanceMethod(self, @selector(initWithTarget:action:)),
                                   class_getInstanceMethod(self, @selector(initWithMockTarget:mockAction:)));
}

-(id)initWithMockTarget:(id)target mockAction:(SEL)action {
    self = [self initWithMockTarget:target mockAction:action];
    self.mockTarget_ = target;
    self.mockAction_ = action;
    self.mockTappedView_ = nil;
    return self;
}

-(UIView *)view {
    return self.mockTappedView_;
}

-(CGPoint)locationInView:(UIView *)view {
    return [view convertPoint:self.mockTappedPoint_ fromView:self.mockTappedView_];
}

//-(UIGestureRecognizerState)state {
//    return UIGestureRecognizerStateEnded;
//}

-(void)performTapWithView:(UIView *)view andPoint:(CGPoint)point {
    self.mockTappedView_ = view;
    self.mockTappedPoint_ = point;

// warning because a leak is possible because the compiler can't tell whether this method
// adheres to standard naming conventions and make the right behavioral decision. Suppress it.
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
    [self.mockTarget_ performSelector:self.mockAction_];
#pragma clang diagnostic pop

}

# pragma mark - Who says we can't add members in a category?

- (void)setMockTappedView_:(UIView *)mockTappedView {
    objc_setAssociatedObject(self, &MockTappedViewKey, mockTappedView, OBJC_ASSOCIATION_ASSIGN);
}

-(UIView *)mockTappedView_ {
    return objc_getAssociatedObject(self, &MockTappedViewKey);
}

- (void)setMockTappedPoint_:(CGPoint)mockTappedPoint {
    objc_setAssociatedObject(self, &MockTappedPointKey, [NSValue value:&mockTappedPoint withObjCType:@encode(CGPoint)], OBJC_ASSOCIATION_COPY);
}

- (CGPoint)mockTappedPoint_ {
    NSValue *value = objc_getAssociatedObject(self, &MockTappedPointKey);
    CGPoint aPoint;
    [value getValue:&aPoint];
    return aPoint;
}

- (void)setMockTarget_:(id)mockTarget {
    objc_setAssociatedObject(self, &MockTargetKey, mockTarget, OBJC_ASSOCIATION_ASSIGN);
}

- (id)mockTarget_ {
    return objc_getAssociatedObject(self, &MockTargetKey);
}

- (void)setMockAction_:(SEL)mockAction {
    objc_setAssociatedObject(self, &MockActionKey, NSStringFromSelector(mockAction), OBJC_ASSOCIATION_COPY);
}

- (SEL)mockAction_ {
    NSString *selectorString = objc_getAssociatedObject(self, &MockActionKey);
    return NSSelectorFromString(selectorString);
}

@end
于 2013-05-09T22:10:04.163 に答える
2

テーブルのタップを処理するView Controllerのテストを自動化するために、テーブルのセルのタップをシミュレートしようとして、同じ問題に直面していました。

コントローラーには、以下のように作成されたプライベート UITapGestureRecognizer があります。

gestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self
  action:@selector(didRecognizeTapOnTableView)];

単体テストでは、ユーザー インタラクションから開始されたときに、gestureRecognizer がアクションをトリガーするように、タッチをシミュレートする必要があります。

このシナリオでは、提案されたソリューションはどれも機能しませんでした。そのため、UITapGestureRecognizer をデコレートして解決し、コントローラーによって呼び出される正確なメソッドを偽造しました。そのため、コントローラー自体がアクションの発生元を認識しない方法でアクションを呼び出す「performTap」メソッドを追加しました。このようにして、トリガーされたアクションだけで、ジェスチャ認識エンジンから独立したコントローラーのテスト ユニットを作成できます。

これは私のカテゴリです。誰かの役に立てば幸いです。

CGPoint mockTappedPoint;
UIView *mockTappedView = nil;
id mockTarget = nil;
SEL mockAction;

@implementation UITapGestureRecognizer (MockedGesture)
-(id)initWithTarget:(id)target action:(SEL)action {
    mockTarget = target;
    mockAction =  action;
    return [super initWithTarget:target action:action];
    // code above calls UIGestureRecognizer init..., but it doesn't matters
}
-(UIView *)view {
    return mockTappedView;
}
-(CGPoint)locationInView:(UIView *)view {
    return [view convertPoint:mockTappedPoint fromView:mockTappedView];
}
-(UIGestureRecognizerState)state {
    return UIGestureRecognizerStateEnded;
}
-(void)performTapWithView:(UIView *)view andPoint:(CGPoint)point {
    mockTappedView = view;
    mockTappedPoint = point;
    [mockTarget performSelector:mockAction];
}

@end
于 2013-04-13T04:39:46.867 に答える