Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Testing if performSegueWithIdentifier is called within a view controllers method

I am going through an application and adding Unit Tests. The application is written using storyboards and supports iOS 6.1 and above.

I have been able to test all the usual return methods with no problem. However I am currently stumped with a certain test I want to perform:

Essentially I have a method, lets call it doLogin:

- (IBAction)doLogin:(UIButton *)sender {

// Some logic here

if ( //certain criteria to meet) {
    variable = x; // important variable set here
    [self performSegueWithIdentifier:@"memorableWord" sender:sender];
} else {
    // handler error here
}

So I want to test that either the segue is called and that the variable is set, or that the MemorableWord view controller is loaded and the variables in there are correct. The variable set here in the doLogin method is passed through to the memorableWord segues' destination view controller in the prepareForSegue method.

I have OCMock set up and working, and I am also using XCTest as my unit testing framework. Has anyone been able to product a unit test to cover such a situation??

It seems that Google and SO are pretty bare in regards to information around this area.. lots of examples on simple basic tests that are pretty irrelevant to the more complex reality of iOS testing.

like image 874
Rob Avatar asked Dec 16 '13 11:12

Rob


4 Answers

You're on the right track, your test wants to check that:

  1. When the login button is tapped doLogin is called with the loginButton as the sender
  2. If some criteria is YES, call performSegue

So you should actually trigger the full flow from login button down to performSegue:

- (void)testLogin {
    LoginViewController *loginViewController = ...;
    id loginMock = [OCMockObject partialMockForObject:loginViewController];

    //here the expect call has the advantage of swallowing performSegueWithIdentifier, you can use forwardToRealObject to get it to go all the way through if necessary
    [[loginMock expect] performSegueWithIdentifier:@"memorableWord" sender:loginViewController.loginButton];

    //you also expect this action to be called
    [[loginMock expect] doLogin:loginViewController.loginButton];

    //mocking out the criteria to get through the if statement can happen on the partial mock as well
    BOOL doSegue = YES;
    [[[loginMock expect] andReturnValue:OCMOCK_VALUE(doSegue)] criteria];

    [loginViewController.loginButton sendActionsForControlEvents:UIControlEventTouchUpInside];

    [loginMock verify]; [loginMock stopMocking];
}

You'll need to implement a property for "criteria" so that there is a getter you can mock using 'expect'.

Its important to realize that 'expect' will only mock out 1 call to the getter, subsequent calls will fail with "Unexpected method invoked...". You can use 'stub' to mock it out for all calls but this means it will always return the same value.

like image 112
ImHuntingWabbits Avatar answered Oct 14 '22 04:10

ImHuntingWabbits


IMHO this seems to be a testing scenario which has not properly been setup.

With unit tests you should only test units (e.g. single methods) of your application. Those units should be independent from all other parts of your application. This will guarantee you that a single function is properly tested without any side effects. BTW: OCMock is great tool to "mock out" all parts you do not want to test and therefore create side effects.

In general your test seems to be more like an integration test

IT is the phase of software testing, in which individual software modules are combined and tested as a group.

So what would I do in your case:

I would either define an integration test, where I would properly test all parts of my view and therefore indirectly test my view controllers. Have a look at a good testing framework for this kind of scenario - KIF

Or I would perform single unit tests on the methods 'doLogin' as well as the method for calculating the criteria within your if statement. All dependencies should be mocked out which means within your doLogin test, you should even mock the criteria method...

like image 43
Alexander Avatar answered Oct 14 '22 03:10

Alexander


So the only way I can see for me to unit test this is using partial mocks:

- (void)testExample
{
    id loginMock = [OCMockObject partialMockForObject:self.controller];

    [[loginMock expect] performSegueWithIdentifier:@"memorableWord" sender:[OCMArg any]];

    [loginMock performSelectorOnMainThread:@selector(loginButton:) withObject:self.controller.loginButton waitUntilDone:YES];

    [loginMock verify];
}

Of course this is only an example of the test and isn't actually the test I am performing, but hopefully demonstrates the way in which I am having to test this method in my view controller. As you can see, if the performSegueWithIdentifier is not called, the verify with cause the test to fail.

like image 2
Rob Avatar answered Oct 14 '22 03:10

Rob


Give OCMock a read, I have just bought a book from amazon about Unit Testing iOS and its really good to read. Looking to get a TDD book too.

like image 1
Tom Avatar answered Oct 14 '22 04:10

Tom