Testing if performSegueWithIdentifier is called within a view controllers method
Asked Answered
P

4

7

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.

Penurious answered 16/12, 2013 at 11:1 Comment(0)
T
4

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.

Twiddle answered 26/12, 2013 at 6:55 Comment(0)
B
2

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...

Beck answered 16/12, 2013 at 11:29 Comment(4)
Thanks, but this is exactly what I am looking to do. I am trying to test the doLogin method on its own without and dependancy on anything else. so for example, as one of my tests, i want to ensure that the method will actually call the performSegueWithIdentifier when the criteria is met. I will also test that the error is handled correctly when it isn't. My problem is finding out weather or not the performSegueWithIdentifier is called or not.. any ideas on this?Penurious
+1 for the decent explanation of the difference between IT an UT, I am sure users who are less familiar with the concept of this will find it useful! :)Penurious
OCMock does have a methods like verify or reject exactly for this kind of purpose. With verify you can ensure that a method has been called and via reject you are able to ensure that a method has NOT been called. Keep in mind that in your case you might need to use a partial mock and that the usage of many partials mocks may be sign of bad application designBeck
I was just implementing the partial mock when you commented, basically that was what I was missing. I am not sure how else you would test if a segue is being called in the code without this, so I would assume that anyone that uses this method to transition between controllers, that wants to cover this code with unit testing, would have a fair few partial mocks in place?Penurious
P
2

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.

Penurious answered 16/12, 2013 at 12:39 Comment(0)
J
1

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.

Judicious answered 4/9, 2014 at 15:53 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.