Reputation: 112857
I am trying to test a method that instantiates an instance of MFMailComposeViewController
. The method being tested calls several methods of MFMailComposeViewController
including setSubject:
.
I want to test that setSubject is sent a specific NSString, in this case @"Test Message".
No matter what I specify for the expected string in the mock stub there is no failure.
In the Unit Test class:
#import <OCMock/OCMock.h>
- (void)testEmail {
TestClass *testInstance = [[TestClass alloc] init];
id mock = [OCMockObject mockForClass:[MFMailComposeViewController class]];
[[mock stub] setSubject:@"Test Message"];
[testInstance testMethod];
}
In TestClass:
- (void)testMethod {
MFMailComposeViewController *mailComposeVC = [[MFMailComposeViewController alloc] init];
[mailComposeVC setSubject:@"Bad Message"];
}
Test Suite 'Email_Tests' started at 2011-09-17 18:12:21 +0000
Test Case '-[Email_Tests testEmail]' started.
Test Case '-[Email_Tests testEmail]' passed (0.041 seconds).
The test should have failed.
I am testing this in the iOS simulator and get the same result on a device.
What am I doing wrong? Is there some way to accomplish this?
Upvotes: 1
Views: 3339
Reputation: 17762
Jon Reid's is a reasonable approach, though it seems that making mailComposeViewController
a class method complicates it. And subclassing it in your test code means you'll always get the mocked version at test time, which may not be what you want. I'd make it an instance method. Then you can use a partial mock to override it at test time:
-(void) testEmail {
TestClass *testInstance = [[[TestClass alloc] init] autorelease];
id mock = [OCMockObject mockForClass:[MFMailComposeViewController class]];
[[mock expect] setSubject:@"Test Message"];
id mockInstance = [OCMockObject partialMockForObject:testInstance];
[[[mockInstance stub] andReturn:mock] mailComposeViewController];
[testInstance testMethod];
[mock verify];
}
If you keep it as a class method, you might consider making it a static global and exposing a way to override it:
static MFMailComposeViewController *mailComposeViewController = nil;
-(id)mailComposeViewController {
if (!mailComposeViewController) {
mailComposeViewController = [[MFMailComposeViewController alloc] init];
}
return mailComposeViewController;
}
-(void)setMailComposeViewController:(MFMailComposeViewController *)controller {
mailComposeViewController = controller;
}
Then, your test would be similar to Jon's example:
-(void)testEmail {
TestClass *testInstance = [[[TestClass alloc] init] autorelease];
id mock = [OCMockObject mockForClass:[MFMailComposeViewController class]];
[[mock expect] setSubject:@"Test Message"];
[testInstance setMailComposeViewController:mock];
[testInstance testMethod];
[mock verify];
// clean up
[testInstance setMailComposeViewController:nil];
}
Upvotes: 2
Reputation: 20980
You create a mock, but never pass it to the class under test, or ask the mock to verify itself. You need some form of dependency injection to say, "Instead of using MFMailComposeViewController, use this other thing I'm giving you."
Here's one way to do that. In the class under test, instead of allocating MFMailComposeViewController directly, get it through a factory method, like this:
@interface TestClass : NSObject
- (void)testMethod;
// Factory methods
+ (id)mailComposeViewController;
@end
Here's the implementation. You were leaking, so note that the factory method returns an autoreleased object.
- (void)testMethod {
MFMailComposeViewController *mailComposeVC =
[[self class] mailComposeViewController];
[mailComposeVC setSubject:@"Bad Message"];
}
+ (id)mailComposeViewController {
return [[[MFMailComposeViewController alloc] init] autorelease];
}
Over on the testing side, we create a testing subclass that overrides the factory method so it provides whatever we want it to:
@interface TestingTestClass : TestClass
@property(nonatomic, assign) id mockMailComposeViewController;
@end
@implementation TestingTestClass
@synthesize mockMailComposeViewController;
+ (id)mailComposeViewController {
return mockMailComposeViewController;
}
@end
Now we're ready for the test. I do a few things differently:
Here's the test:
- (void) testEmail {
TestClass *testInstance = [[[TestClass alloc] init] autorelease];
id mock = [OCMockObject mockForClass:[MFMailComposeViewController class]];
[[mock expect] setSubject:@"Test Message"];
[testInstance setMockMailComposeViewController:mock];
[testInstance testMethod];
[mock verify];
}
For completeness, we need one final test, and that's to guarantee that the factory method in the actual class returns what we expect:
- (void)testMailComposerViewControllerShouldBeCorrectType {
STAssertTrue([[TestClass mailComposeViewController]
isKindOfClass:[MFMailComposeViewController class]], nil);
}
Upvotes: 5