Zhang
Zhang

Reputation: 11607

iOS unit testing button has action assigned

I've got the following button written programmatically:

self.button = [[UIButton alloc] init];
[self.button setTitle:@"Reverse String!" forState:UIControlStateNormal];
self.button.backgroundColor = [UIColor colorWithRed:0.2 green:0.2 blue:0.2 alpha:1.0];
self.button.layer.cornerRadius = 5.0;
[self.button addTarget:self action:@selector(reverseString:) forControlEvents:UIControlEventTouchUpInside];

However, my unit test fails when I try to test:

-(void)testButtonActionAssigned
{
    XCTAssert([self.vc.button respondsToSelector:@selector(reverseString:)]);
}

I get a failed message saying:

test failure: -[ViewControllerTests testButtonActionAssigned] failed: (([self.vc.button respondsToSelector:@selector(reverseString:)]) is true) failed

My setup method is called:

- (void)setUp {
    [super setUp];
    // Put setup code here. This method is called before the invocation of each test method in the class.
    self.vc = [[ViewController alloc] init];

    NSLog(@"THIS METHOD IS CALLED");
}

Is it to do with the simulator timing and startup ?

Upvotes: 1

Views: 1254

Answers (2)

Zhang
Zhang

Reputation: 11607

I got it. I was testing the wrong thing.

The code in the question isn't really testing the button has an action assigned. It's basically saying "does the view controller responds to this method" or "does the view controller have this method".

// this is saying "does the ViewController know about this method"
// rather than "is ViewController's button assigned method reverseString:"
XCTAssert([self.vc respondsToSelector:...]);

This means regardless of whether button has a selector assigned or not, as long as the method is defined, this assert is always true.

I verified it by commenting out the line of code:

[self.button addTarget:self action:@selector(reverseString:) forControlEvents:UIControlEventTouchUpInside];

and the test still passes, which isn't what I was trying to achieve.

The proper test should be:

-(void)testButtonActionAssigned
{
    [self.vc viewDidLoad];

    NSArray *arrSelectors = [self.vc.button actionsForTarget:self.vc forControlEvent:UIControlEventTouchUpInside];

    NSString *selector = nil;

    if(arrSelectors.count > 0)
    {
        selector = arrSelectors[0];
    }

    XCTAssert([selector isEqualToString:@"reverseString:"]);
}

Now after commenting out the line:

[self.button addTarget:self action:@selector(reverseString:) forControlEvents:UIControlEventTouchUpInside];

The test fails as expected :D

This proper test code fetches the selector from the button and checks to see if it indeed is the method called reverseString:

EDIT

After B.S. answer, and confirming with Apple's documentation, a shorter version can be written as:

-(void)testButtonActionAssigned
{
    [self.vc viewDidLoad];

    NSString *selector = [[self.vc.button actionsForTarget:self.vc forControlEvent:UIControlEventTouchUpInside] firstObject];

    XCTAssert([selector isEqualToString:@"reverseString:"]);
}

I was originally worried that calling firstObject on an empty array would cause an app to crash but Apple's documentation has confirmed to me that firstObject is a property that returns nil if array is empty.

The first object in the array. (read-only)

Declaration @property(nonatomic, readonly) ObjectType firstObject

Discussion If the array is empty, returns nil.

https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSArray_Class/#//apple_ref/occ/instp/NSArray/firstObject

Upvotes: 0

B.S.
B.S.

Reputation: 21726

[self.button addTarget:self action:@selector(reverseString:) forControlEvents:UIControlEventTouchUpInside];   

First line means that reverseString: method should be in your ViewController (which is specified by target: parameter and you pass self there). So you need to change test with just removing .button

 -(void)testButtonActionAssigned
    {
        XCTAssert([self.vc respondsToSelector:@selector(reverseString:)]);
    }

Here is small example which can be changed:

UIButton *button = [UIButton new];
[button addTarget:self action:@selector(doThat:) forControlEvents:UIControlEventTouchUpInside];

NSString *selectorString = [[button actionsForTarget:self forControlEvent:UIControlEventTouchUpInside] firstObject];

SEL sel = NSSelectorFromString(selectorString);

// so now you can check if target responds to selector
// BOOL responds = [self respondsToSelector:sel];

NSLog(@"%@", selectorString);

Upvotes: 1

Related Questions