使用从测试方法调用的方法的类的 OCMock

Posted

技术标签:

【中文标题】使用从测试方法调用的方法的类的 OCMock【英文标题】:OCMock of class with method called from the test method 【发布时间】:2011-09-17 18:17:41 【问题描述】:

我正在尝试测试一个实例化MFMailComposeViewController 实例的方法。被测试的方法调用了MFMailComposeViewController的几个方法,包括setSubject:

我想测试 setSubject 是否发送了一个特定的 NSString,在本例中为 @"Test Message"。 无论我为模拟存根中的预期字符串指定什么,都不会失败。

在单元测试类中:

#import <OCMock/OCMock.h>

- (void)testEmail 
    TestClass *testInstance = [[TestClass alloc] init];

    id mock = [OCMockObject mockForClass:[MFMailComposeViewController class]];
    [[mock stub] setSubject:@"Test Message"];

    [testInstance testMethod];

在测试类中:

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

测试应该失败了。

我正在 ios 模拟器中对此进行测试,并在设备上得到相同的结果。

我做错了什么?有没有办法做到这一点?

【问题讨论】:

【参考方案1】:

您创建了一个模拟,但从不将其传递给被测类,或要求模拟验证自己。你需要某种形式的依赖注入来表示,“不要使用 MFMailComposeViewController,而是使用我给你的其他东西。”

这是一种方法。在被测类中,不是直接分配MFMailComposeViewController,而是通过工厂方法获取,如下:

@interface TestClass : NSObject

- (void)testMethod;

// Factory methods
+ (id)mailComposeViewController;

@end

这里是实现。您正在泄漏,因此请注意工厂方法返回一个自动释放的对象。

- (void)testMethod 
    MFMailComposeViewController *mailComposeVC =
                                    [[self class] mailComposeViewController];
    [mailComposeVC setSubject:@"Bad Message"];


+ (id)mailComposeViewController 
    return [[[MFMailComposeViewController alloc] init] autorelease];

在测试方面,我们创建了一个覆盖工厂方法的测试子类,以便它提供我们想要的任何东西:

@interface TestingTestClass : TestClass
@property(nonatomic, assign) id mockMailComposeViewController;
@end

@implementation TestingTestClass
@synthesize mockMailComposeViewController;

+ (id)mailComposeViewController 
    return mockMailComposeViewController;


@end

现在我们准备好进行测试了。我做一些不同的事情:

分配测试子类而不是实际类(不要泄漏!) 使用期望设置模拟,而不仅仅是存根 将模拟注入到测试子类中 最后验证模拟

这是测试:

- (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];

为了完整起见,我们需要最后一个测试,这是为了保证实际类中的工厂方法返回我们期望的结果:

- (void)testMailComposerViewControllerShouldBeCorrectType 
    STAssertTrue([[TestClass mailComposeViewController]
                 isKindOfClass:[MFMailComposeViewController class]], nil);

【讨论】:

感谢您的方法和回答!我正在学习如何为单元可测试性编写 Uint 测试和代码。在这种情况下,我将单元测试添加到遗留代码中,因此希望在我将它们到位之前不对其进行任何更改或进行最小更改。有一点,我确实尝试为MFMailComposeViewController 传递一个模拟,它适用于通过测试,但导致测试失败导致崩溃,这对于 OCMock 来说是否常见?此外,为了清楚起见,我故意省略了内存管理,尽管我倾向于尽可能使用 ARC 编写代码。 OCMock 通过引发异常来报告失败。 SenTestingKit 旨在捕获和报告异常,但不幸的是,由于模拟器中的错误,它反而崩溃了。我正在开发一个可以避免这种情况的替代模拟框架,但它还没有准备好。【参考方案2】:

Jon Reid 的方法是一种合理的方法,尽管使 mailComposeViewController 成为类方法似乎会使它复杂化。在您的测试代码中对其进行子类化意味着您将始终在测试时获得模拟版本,这可能不是您想要的。我会把它变成一个实例方法。然后您可以在测试时使用部分模拟来覆盖它:

-(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];

如果您将其保留为类方法,您可能会考虑将其设为静态全局并公开一种覆盖它的方法:

static MFMailComposeViewController *mailComposeViewController = nil;

-(id)mailComposeViewController 
    if (!mailComposeViewController) 
        mailComposeViewController = [[MFMailComposeViewController alloc] init];
    
    return mailComposeViewController;


-(void)setMailComposeViewController:(MFMailComposeViewController *)controller 
    mailComposeViewController = controller;

那么,您的测试将类似于 Jon 的示例:

-(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];

【讨论】:

关于部分模拟的好建议!

以上是关于使用从测试方法调用的方法的类的 OCMock的主要内容,如果未能解决你的问题,请参考以下文章

使用具有未知数量的方法调用的 OCMock

模拟另一个类方法的内部调用

我可以使用 OCMock 测试是不是调用了超类吗?

OCMock 对带有参数的方法进行 OCMock 并返回一个值

如何使用 ocmock 在基于类的方法上存根返回值

OCMock 测试类别方法