Wednesday, May 8, 2013

Day 14: TDD

I have been a long-time proponent of the principles and benefits of Test-Driven Development. All of my Android projects use Robolectric and the built-in instrumentation testing framework. So naturally, when I decided to explore iOS, testing was one of the first things I wanted to investigate.

Fortunately there is no shortage of quality testing frameworks for iOS. I decided to focus my efforts on 3 of the available frameworks:

OCUnit

Based on the SenTest framework, OCUnit is a traditional xUnit style unit testing tool that comes bundled with the iOS SDK.

Kiwi

An RSpec style testing tool with an extensible expectation syntax and built-in mocking tools for Behavior-Driven Development (BDD).

KIF

A fully automated functional testing tool that simulates user interactions by leveraging accessibility attributes provided by the OS.


Three the Hard Way (HelloTDDiOS)


Three the Hard Way is a sample application I created to showcase all 3 testing frameworks in action. The app itself is very simple but provides a good forum for experimenting with various testing techniques.

Based on Your first iOS App tutorial on Apple Developer, Three the Hard Way allows you to enter a name via text input, click a button, and view the resulting greeting.

       


Development of the app was 100% test-driven using OCUnit. Below is an example of tests written to exercise the button callback in the view controller.

@implementation HelloTDDViewControllerTests {
HelloTDDViewController *viewController;
}
- (void)setUp {
viewController = [[HelloTDDViewController alloc] init];
}
//...
- (void)testOnButtonClickNotifiesDelegate {
MockHelloTDDViewControllerDelegate *delegate =
[[MockHelloTDDViewControllerDelegate alloc] init];
viewController.delegate = delegate;
[viewController onButtonClick:nil];
BOOL result = delegate.buttonWasClicked;
STAssertTrue(result,
@"View controller should notify delegate on button click.");
}
- (void)testOnButtonClickPassesNameToDelegate {
MockHelloTDDViewControllerDelegate *delegate =
[[MockHelloTDDViewControllerDelegate alloc] init];
viewController.delegate = delegate;
UITextField *textField = [[UITextField alloc] init];
textField.text = @"First Last";
viewController.nameField = textField;
[viewController onButtonClick:nil];
STAssertEqualObjects(delegate.lastNameSent, @"First Last",
@"View controller should pass name to delegate.");
}
//...
@end
And the production code that was written to satisfy these tests. Notice how the tests use an instance of MockHelloTDDViewControllerDelegate to test the view controller in isolation rather than GreetingFactory (the actual delegate used in production).

@implementation HelloTDDViewController
- (void)viewDidLoad {
[super viewDidLoad];
GreetingFactory *greetingFactory = [[GreetingFactory alloc] init];
_delegate = greetingFactory;
}
//...
- (IBAction)onButtonClick:(id)sender {
NSString *name = _nameField.text;
[_delegate sayHello:name toMyLittleFriend:self];
}
//...
@end
And here are the same tests rewritten using Kiwi.

SPEC_BEGIN(HelloTDDViewControllerSpec)
describe(@"HelloTDDViewController", ^{
HelloTDDViewController *viewController =
[[HelloTDDViewController alloc] init];
MockHelloTDDViewControllerDelegate *delegate =
[[MockHelloTDDViewControllerDelegate alloc] init];
//...
context(@"when button is pressed", ^{
it(@"should notify delegate.", ^{
viewController.delegate = delegate;
[viewController onButtonClick:nil];
[[theValue(delegate.buttonWasClicked) should] beYes];
});
it(@"should pass name to delegate.", ^{
viewController.delegate = delegate;
UITextField *textField = [[UITextField alloc] init];
textField.text = @"First Last";
viewController.nameField = textField;
[viewController onButtonClick:nil];
[[delegate.lastNameSent should] equal:@"First Last"];
});
});
//...
});
SPEC_END
Finally, here is the end-to-end integration test using KIF that tests the button through a scripted UI interaction.

@implementation HelloTDDTestController
- (void)initializeScenarios {
[self addScenario:[KIFTestScenario scenarioToDisplayGreeting]];
}
@end
@implementation KIFTestScenario (HelloTDDAdditions)
+ (id)scenarioToDisplayGreeting {
KIFTestScenario *scenario =
[KIFTestScenario scenarioWithDescription:
@"Test greeting with name."];
[scenario addStep:
[KIFTestStep stepToEnterText:
@"Chuck Norris"intoViewWithAccessibilityLabel:@"Name"]];
[scenario addStep:
[KIFTestStep stepToTapViewWithAccessibilityLabel:
@"Say Hello"]];
[scenario addStep:
[KIFTestStep stepToVerifyGreeting:
@"Hello, Chuck Norris!"]];
return scenario;
}
@end
@implementation KIFTestStep (HelloTDDAdditions)
+ (id)stepToVerifyGreeting:(NSString *)expectedLabel {
NSString *description =
[NSString stringWithFormat:@"Verify output is '%@'",
expectedLabel];
return [self stepWithDescription:description
executionBlock:^(KIFTestStep *step, NSError **error) {
UIAccessibilityElement *element =
[[UIApplication sharedApplication]
accessibilityElementWithLabel:@"Greeting"];
UILabel *label = (UILabel *)[UIAccessibilityElement
viewContainingAccessibilityElement:element];
if ([expectedLabel isEqualToString:label.text]) {
return KIFTestStepResultSuccess;
}
KIFTestCondition(NO, error,
@"Failed to compare the label text: expected '%@', actual '%@'",
expectedLabel, label.text);
}];
}
@end
In order for KIF to locate the UI elements, we must set the accessibility labels in (void)viewDidLoad.

@implementation HelloTDDViewController
- (void)viewDidLoad {
[super viewDidLoad];
_nameField.accessibilityLabel = @"Name";
_helloLabel.accessibilityLabel = @"Greeting";
GreetingFactory *greetingFactory = [[GreetingFactory alloc] init];
_delegate = greetingFactory;
}
//...
@end
Fork the full project at https://github.com/ecgreb/Three-the-Hard-Way. Feedback is welcome.

1 comment:

  1. I'd also recommend checking out specta.

    https://github.com/specta/specta

    ReplyDelete