Howto dynamic steps with KIF

One of my latest tasks was to write some KIF. tests for our iOS applications. If you are not familiar what KIF. is or what it does, here is a brief intro...
KIF. is short for Keep It Functional. It is an open source test framework written by Square Inc and hosted on GitHub.
Now that you are somewhat familiar with what KIF is and how to use it, this guide will show you how you can have more or less dynamic steps. By dynamic, I mean that you could have a setup that looks like this:

                            Step if passed
                           /
                       PASS
                        |
Step to execute -------< >
                        |
                       FAIL
                           \
                            Step if failed

NOTE: While this could be a very desired behavior for your test case, you should try to avoid it by any means possible. A while back, I've asked this quesiton on KIF's Google Groups forums and Conrad Stoll gave me a very good advice that I, and everyone else, should adhere to.

If you are still convinced that there is no other way for you to write your test case and y ou need that dynamic functionality, then read on...

The way KIF is designed is that every step in your scenario gest preloaded into a step queue and then these steps get executed one after another. So, when you create your scenario in a way similar to this:

+(KIFTestScenario *) mySuperAwesomeTestScenario;
{
	KIFTestScenario *scenario = [KIFTestScenario scenarioWithDescription:@"My Super Awesome Test Scenario"];
	[scenario addStep:[KIFTestStep stepToTapViewWithAccessibilityLabel:@"My Label"]];
	if (previous step succeeded) {
		[scenario addStep: [KIFTestStep stepToTapViewWithAccessibilityLabel@"Button A"]];
	}
	else {
		[scenario addStep: [KIFTestStep stepToTapViewWithAccessibilityLabel@"Button B"]];
	}
 
	[scenario addStep: [KIFTestStep stepToTapViewWithAccessibilityLabel@"Button C"]];
 
	return scenario;
}

it will NOT work!

Instead, you will want to create a custom step that will handle that logic internally.

To do that, you will want to do something like this:

KIFTestStep *step = [KIFTestStep stepWithDescription:@"My Test Step" executionBlock:^KIFTestStepResult(KIFTestStep *step, NSError *__autoreleasing *error) {
        KIFTestStep *findCtrl = [KIFTestStep stepToWaitForViewWithAccessibilityLabel:@"failingAccessLabel"];
 
        KIFTestStepResult result = KIFTestStepResultFailure;
        // loop until step.timeout is expired or step either passed or failed
        for (int i = 0; i < step.timeout / 2; i++) {
            result = [findCtrl executeAndReturnError:error];
            if ((result == KIFTestStepResultSuccess) || (result == KIFTestStepResultFailure)) {
                break;
            }
            // wait 2 seconds before trying again
            CFRunLoopRunInMode(kCFRunLoopDefaultMode, 2.0, false);
        }
 
        if (result == KIFTestStepResultSuccess) {
            NSLog(@"Success");
            return KIFTestStepResultSuccess;
        }
        else {
            if (result == KIFTestStepResultFailure) {
                NSLog(@"An actual failed result");
            }
            else {
                NSLog(@"We have still received a 'Wait' result");
                NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:@"View was not found", NSLocalizedDescriptionKey, nil];
                *error = [[NSError alloc] initWithDomain:@"KIFTest" code:KIFTestStepResultFailure userInfo:dict];
            }
 
            return KIFTestStepResultFailure;
        }
    }];

To take it one step further, you could add the following method to your step library that would make this code reusable like so:

+(KIFTestStep *) stepToExecuteAStep:(KIFTestStep *)stepToExecute withStepIfPassed:(KIFTestStep *)passStep withStepIfFailed:(KIFTestStep *)failStep;
{
	KIFTestStep *retVal = [KIFTestStep stepWithDescription:description executionBlock:^KIFTestStepResult(KIFTestStep *step, NSError *__autoreleasing *error) {
		// initialize result variable
		KIFTestStepResult result = KIFTestStepResultFailure;
 
		// execute the main step
		for (int i = 0; i < stepToExecute.timeout / 2; i++) {
			result = [stepToExecute executeAndReturnError:error];
			if (result == KIFTestStepResultWait) {
				CFRunLoopRunInMode(kCFRunLoopDefaultMode, 2.0, false);
			}
			else {
				break;
			}
		}
 
		if (result == KIFTestStepResultSuccess) {
			// run your 'step if passed' step
			return [passStep executeAndReturnError:error];
		}
		else if (failStep) {
			return [failStep executeAndReturnError:error];
		}
		else {
			return KIFTestStepResultFailure;
		}
	}];
 
	return retVal;
}

The method above should be used in the following manner:

...
KIFTestStep *mainStep = [KIFTestStep stepToWaitForViewWithAccessibilityLabel:@"Username"];
KIFTestStep *passStep = [KIFTestStep stepToEnterText:@"my login" intoViewWithAccessibilityLabel:@"Username"];
KIFTestStep *failStep = [KIFTestStep stepToWaitForTimeInterval:5.0 withDescription:@"Could not find 'Username' field, but test should not fail."];
[scenario addStep:[KIFTestSTep stepToExecuteAStep:mainStep withStepIfPassed:passStep withStepIfFailed:failStep]];
...

Hope this helps!

As always, questions and comments are welcomed.

Comments