使用OCUnit的单元测试示例 [英] Unit Testing Example with OCUnit

查看:116
本文介绍了使用OCUnit的单元测试示例的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我真的很难理解单元测试。我确实理解TDD的重要性,但我读到的所有单元测试的例子似乎都非常简单和微不足道。例如,测试以确保设置属性或将内存分配给数组。为什么?如果我编码 .. alloc] init] ,我真的需要确保它有效吗?

I'm really struggling to understand unit testing. I do understand the importance of TDD, but all the examples of unit testing I read about seem to be extremely simple and trivial. For example, testing to make sure a property is set or if memory is allocated to an array. Why? If I code out ..alloc] init], do I really need to make sure it works?

I我很擅长开发,所以我确信我在这里缺少一些东西,特别是围绕TDD的所有热潮。

I'm new to development so I'm sure I'm missing something here, especially with all the craze surrounding TDD.

我认为我的主要问题是找不到任何实际的例子。这是一个方法 setReminderId ,似乎是一个很好的测试候选人。什么是有用的单元测试看起来确保这是有效的? (使用OCUnit)

I think my main issue is I can't find any practical examples. Here is a method setReminderId that seems to be a good candidate for testing. What would a useful unit test look like to make sure this is working? (using OCUnit)

- (NSNumber *)setReminderId: (NSDictionary *)reminderData
{
    NSNumber *currentReminderId = [[NSUserDefaults standardUserDefaults] objectForKey:@"currentReminderId"];
    if (currentReminderId) {
        // Increment the last reminderId
        currentReminderId = @(currentReminderId.intValue + 1);
    }
    else {
        // Set to 0 if it doesn't already exist
        currentReminderId = @0;
    }
    // Update currentReminderId to model
    [[NSUserDefaults standardUserDefaults] setObject:currentReminderId forKey:@"currentReminderId"];

    return currentReminderId;
}


推荐答案

更新:我在两个方面改进了这个答案:它现在是一个截屏视频,我从属性注入切换到构造函数注入。请参阅如何开始使用Objective-C TDD

棘手的部分是该方法依赖于外部对象NSUserDefaults。我们不想直接使用NSUserDefaults。相反,我们需要以某种方式注入此依赖关系,以便我们可以替换假的用户默认值进行测试。

The tricky part is that the method has a dependency on an external object, NSUserDefaults. We don't want to use NSUserDefaults directly. Instead, we need to inject this dependency somehow, so that we can substitute a fake user defaults for testing.

有几种不同的方法可以做到这一点。一种方法是将其作为方法的额外参数传递。另一种方法是使它成为类的实例变量。并且有不同的方法来设置这个ivar。有构造函数注入,它在初始化程序参数中指定。或者是财产注入。对于iOS SDK中的标准对象,我的首选是使其成为一个属性,具有默认值。

There are a few different ways of doing this. One is by passing it in as an extra argument to the method. Another is to make it an instance variable of the class. And there are different ways of setting up this ivar. There's "constructor injection" where it's specified in the initializer arguments. Or there's "property injection." For standard objects from the iOS SDK, my preference is to make it a property, with a default value.

所以让我们从属性的测试开始,默认情况下,NSUserDefaults。顺便说一下,我的工具集是Xcode的内置OCUnit,还有 OCHamcrest 用于断言和 OCMockito 用于模拟对象。还有其他选择,但这就是我使用的。

So let's start with a test that the property is, by default, NSUserDefaults. My toolset, by the way, is Xcode's built-in OCUnit, plus OCHamcrest for assertions and OCMockito for mock objects. There are other choices, but that's what I use.

缺乏更好的名称,该类将被命名为示例。对于受测试的系统,该实例将被命名为 sut 。该属性将命名为 userDefaults 。这是在ExampleTests.m中确定其默认值的第一个测试:

For lack of a better name, the class will be named Example. The instance will be named sut for "system under test." The property will be named userDefaults. Here's a first test to establish what its default value should be, in ExampleTests.m:

#import <SenTestingKit/SenTestingKit.h>

#define HC_SHORTHAND
#import <OCHamcrestIOS/OCHamcrestIOS.h>

@interface ExampleTests : SenTestCase
@end

@implementation ExampleTests

- (void)testDefaultUserDefaultsShouldBeSet
{
    Example *sut = [[Example alloc] init];
    assertThat([sut userDefaults], is(instanceOf([NSUserDefaults class])));
}

@end

在这个阶段,这不是不能编译 - 因为测试失败了。仔细看看。如果你可以让你的眼睛跳过括号和括号,测试应该非常清楚。

At this stage, this doesn't compile — which counts as the test failing. Look it over. If you can get your eyes to skip over the brackets and parentheses, the test should be pretty clear.

让我们编写最简单的代码来获得编译的测试和跑 - 失败。这是Example.h:

Let's write the simplest code we can to get that test to compile and run — and fail. Here's Example.h:

#import <Foundation/Foundation.h>

@interface Example : NSObject
@property (strong, nonatomic) NSUserDefaults *userDefaults;
@end

令人敬畏的Example.m:

And the awe-inspiring Example.m:

#import "Example.h"

@implementation Example
@end

我们需要在ExampleTests.m的最开头添加一行:

We need to add a line to the very beginning of ExampleTests.m:

#import "Example.h"

测试运行,并且失败并显示消息预期NSUserDefaults的实例,但是为零。正是我们想要的。我们已经完成了第一次测试的第1步。

The test runs, and fails with the message, "Expected an instance of NSUserDefaults, but was nil". Exactly what we wanted. We have reached step 1 of our first test.

第2步是编写我们可以通过该测试的最简单代码。怎么样:

Step 2 is to write the simplest code we can to pass that test. How about this:

- (id)init
{
    self = [super init];
    if (self)
        _userDefaults = [NSUserDefaults standardUserDefaults];
    return self;
}

它通过!第2步已完成。

It passes! Step 2 is complete.

第3步是重构代码以包含生产代码和测试代码中的所有更改。但是还没有什么可以清理的。我们完成了第一次测试。到目前为止我们有什么?可以访问 NSUserDefaults 的类的开头,但也可以覆盖它进行测试。

Step 3 is to refactor code to incorporate all changes, in both production code and test code. But there's really nothing to clean up yet. We are done with our first test. What do we have so far? The beginnings of a class that can access NSUserDefaults, but also have it overridden for testing.

现在让我们为该方法编写一个测试。我们想要它做什么?如果用户默认值没有匹配的键,我们希望它返回0.。

Now let's write a test for the method. What do we want it to do? If the user defaults has no matching key, we want it to return 0.

首次使用模拟对象时,我建议先手工制作,以便你了解他们的目标。然后开始使用模拟对象框架。但是我要继续前进并使用OCMockito来加快速度。我们将这些行添加到ExampleTest.m中:

When first starting with mock objects, I recommend making them by hand at first, so that you get an idea of what they're for. Then start using a mock object framework. But I'm going to jump ahead and use OCMockito to make things faster. We add these lines to the ExampleTest.m:

#define MOCKITO_SHORTHAND
#import <OCMockitoIOS/OCMockitoIOS.h>

默认情况下,基于OCMockito的模拟对象将返回 nil 任何方法。但是我会编写额外的代码来明确期望,因为它被要求 objectForKey:@currentReminderId,它将返回。考虑到所有这些,我们希望该方法返回NSNumber 0.(我不会传递参数,因为我不知道它是什么。我将命名方法 nextReminderId 。)

By default, an OCMockito-based mock object will return nil for any method. But I'll write extra code to make the expectation explicit by saying, "given that it's asked for objectForKey:@"currentReminderId", it will return nil." And given all that, we want the method to return the NSNumber 0. (I'm not going to pass an argument, because I don't know what it's for. And I'm going to name the method nextReminderId.)

- (void)testNextReminderIdWithNoCurrentReminderIdInUserDefaultsShouldReturnZero
{
    Example *sut = [[Example alloc] init];
    NSUserDefaults *mockUserDefaults = mock([NSUserDefaults class]);
    [sut setUserDefaults:mockUserDefaults];
    [given([mockUserDefaults objectForKey:@"currentReminderId"]) willReturn:nil];

    assertThat([sut nextReminderId], is(equalTo(@0)));
}

这还没有编译。让我们在Example.h中定义 nextReminderId 方法:

This doesn't compile yet. Let's define the nextReminderId method in Example.h:

- (NSNumber *)nextReminderId;

这是Example.m中的第一个实现。我希望测试失败,所以我要返回一个伪造的数字:

And here's the first implementation in Example.m. I want the test to fail, so I'm going to return a bogus number:

- (NSNumber *)nextReminderId
{
    return @-1;
}

测试失败并显示消息预期< 0,但是< -1>。测试失败很重要,因为这是我们测试测试的方式,并确保我们编写的代码将其从失败状态转换为传递状态。第1步已完成。

The test fails with the message, "Expected <0>, but was <-1>". It's important that the test fail, because it's our way of testing the test, and ensuring that the code we write flips it from a failing state to a passing state. Step 1 is complete.

第2步:让我们通过测试测试。但请记住,我们想要通过测试的最简单的代码。这看起来非常愚蠢。

Step 2: Let's get the test test to pass. But remember, we want the simplest code that passes the test. It's going to look awfully silly.

- (NSNumber *)nextReminderId
{
    return @0;
}

太棒了,它过去了!但我们还没有完成这项测试。现在我们来到第3步:重构。测试中有重复的代码。让我们将 sut (被测系统)拉入ivar。我们将使用 -setUp 方法进行设置,并使用 -tearDown 进行清理(销毁它) 。

Amazing, it passes! But we're not done with this test yet. Now we come to Step 3: refactor. There's duplicate code in the tests. Let's pull sut, the system under test, into an ivar. We'll use the -setUp method to set it up, and -tearDown to clean it up (destroying it).

@interface ExampleTests : SenTestCase
{
    Example *sut;
}
@end

@implementation ExampleTests

- (void)setUp
{
    [super setUp];
    sut = [[Example alloc] init];
}

- (void)tearDown
{
    sut = nil;
    [super tearDown];
}

- (void)testDefaultUserDefaultsShouldBeSet
{
    assertThat([sut userDefaults], is(instanceOf([NSUserDefaults class])));
}

- (void)testNextReminderIdWithNoCurrentReminderIdInUserDefaultsShouldReturnZero
{
    NSUserDefaults *mockUserDefaults = mock([NSUserDefaults class]);
    [sut setUserDefaults:mockUserDefaults];
    [given([mockUserDefaults objectForKey:@"currentReminderId"]) willReturn:nil];

    assertThat([sut nextReminderId], is(equalTo(@0)));
}

@end

我们再次运行测试,确保他们仍然通过,他们做到了。重构只能在绿色或通过状态下进行。无论是在测试代码还是生产代码中完成重构,所有测试都应该继续通过。

We run the tests again, to make sure they still pass, and they do. Refactoring should only be done in "green" or passing state. All tests should continue to pass, whether refactoring is done in the test code or the production code.

现在让我们测试另一个要求:应该保存用户默认值。我们将使用与先前测试相同的条件。但是我们创建了一个新测试,而不是在现有测试中添加更多断言。理想情况下,每个测试都应验证一件事,并且要有一个好的名称来匹配。

Now let's test another requirement: the user defaults should be saved. We'll use the same conditions as the previous test. But we create a new test, instead of adding more assertions to the existing test. Ideally, each test should verify one thing, and have a good name to match.

- (void)testNextReminderIdWithNoCurrentReminderIdInUserDefaultsShouldSaveZeroInUserDefaults
{
    // given
    NSUserDefaults *mockUserDefaults = mock([NSUserDefaults class]);
    [sut setUserDefaults:mockUserDefaults];
    [given([mockUserDefaults objectForKey:@"currentReminderId"]) willReturn:nil];

    // when
    [sut nextReminderId];

    // then
    [verify(mockUserDefaults) setObject:@0 forKey:@"currentReminderId"];
}

验证声明是OCMockito的说法,这个模拟对象应该这样被称为一次。我们运行测试并失败,预期1匹配调用,但收到0。第1步已完成。

The verify statement is the OCMockito way of saying, "This mock object should have been called this way one time." We run the tests and get a failure, "Expected 1 matching invocation, but received 0". Step 1 is complete.

第2步:传递的最简单代码。准备?这里是:

Step 2: the simplest code that passes. Ready? Here goes:

- (NSNumber *)nextReminderId
{
    [_userDefaults setObject:@0 forKey:@"currentReminderId"];
    return @0;
}

但为什么要保存 @ 0 在用户默认值中,而不是具有该值的变量?你问。因为这是我们测试过的。坚持下去,我们会到达那里。

"But why are you saving @0 in user defaults, instead of a variable with that value?" you ask. Because that's as far as we've tested. Hang on, we'll get there.

第3步:重构。同样,我们在测试中有重复的代码。让我们把 mockUserDefaults 作为一个ivar。

Step 3: refactor. Again, we have duplicate code in the tests. Let's pull out mockUserDefaults as an ivar.

@interface ExampleTests : SenTestCase
{
    Example *sut;
    NSUserDefaults *mockUserDefaults;
}
@end

测试代码显示警告,本地声明'mockUserDefaults'隐藏了实例变量。修复它们以使用ivar。然后让我们提取一个帮助方法,以便在每个测试开始时建立用户默认值的条件。让我们将 nil 拉出到一个单独的变量来帮助我们进行重构:

The test code shows warnings, "Local declaration of 'mockUserDefaults' hides instance variable". Fix them to use the ivar. Then let's extract a helper method to establish the condition of the user defaults at the start of each test. Let's pull that nil out to a separate variable to help us with the refactoring:

    NSNumber *current = nil;
    mockUserDefaults = mock([NSUserDefaults class]);
    [sut setUserDefaults:mockUserDefaults];
    [given([mockUserDefaults objectForKey:@"currentReminderId"]) willReturn:current];

现在选择最后3行,单击上下文,然后选择Refactor▶Extract。我们将创建一个名为的新方法setUpUserDefaultsWithCurrentReminderId:

Now select the last 3 lines, context click, and select Refactor ▶ Extract. We'll make a new method called setUpUserDefaultsWithCurrentReminderId:

- (void)setUpUserDefaultsWithCurrentReminderId:(NSNumber *)current
{
    mockUserDefaults = mock([NSUserDefaults class]);
    [sut setUserDefaults:mockUserDefaults];
    [given([mockUserDefaults objectForKey:@"currentReminderId"]) willReturn:current];
}

现在调用它的测试代码如下所示:

The test code that invokes this now looks like:

    NSNumber *current = nil;
    [self setUpUserDefaultsWithCurrentReminderId:current];

该变量的唯一原因是帮助我们进行自动重构。让我们将其内联:

The only reason for that variable was to help us with the automated refactoring. Let's inline it away:

    [self setUpUserDefaultsWithCurrentReminderId:nil];

测试仍然通过。由于Xcode的自动重构没有通过调用新的帮助器方法替换该代码的所有实例,因此我们需要自己完成。所以现在测试看起来像这样:

Tests still pass. Since Xcode's automated refactoring didn't replace all instances of that code with a call to the new helper method, we need to do that ourselves. So now the tests look like this:

- (void)testNextReminderIdWithNoCurrentReminderIdInUserDefaultsShouldReturnZero
{
    [self setUpUserDefaultsWithCurrentReminderId:nil];

    assertThat([sut nextReminderId], is(equalTo(@0)));
}

- (void)testNextReminderIdWithNoCurrentReminderIdInUserDefaultsShouldSaveZeroInUserDefaults
{
    // given
    [self setUpUserDefaultsWithCurrentReminderId:nil];

    // when
    [sut nextReminderId];

    // then
    [verify(mockUserDefaults) setObject:@0 forKey:@"currentReminderId"];
}

了解我们如何继续清洁?测试实际上变得更容易阅读!

See how we continually clean as we go? The tests have actually become easier to read!

现在我们想测试一下,如果用户默认值有一些值,我们返回一个更大的值。我将复制并更改应该返回零测试,使用任意值3。

Now we want to test that if the user defaults has some value, we return one greater. I'm going to copy and alter the "should return zero" test, using an arbitrary value of 3.

- (void)testNextReminderIdWithCurrentReminderIdInUserDefaultsShouldReturnOneGreater
{
    [self setUpUserDefaultsWithCurrentReminderId:@3];

    assertThat([sut nextReminderId], is(equalTo(@4)));
}

根据需要失败:预期< 4>,但是< ; 0>。

That fails, as desired: "Expected <4>, but was <0>".

这是通过测试的简单代码:

Here's simple code to pass the test:

- (NSNumber *)nextReminderId
{
    NSNumber *reminderId = [_userDefaults objectForKey:@"currentReminderId"];
    if (reminderId)
        reminderId = @([reminderId integerValue] + 1);
    else
        reminderId = @0;
    [_userDefaults setObject:@0 forKey:@"currentReminderId"];
    return reminderId;
}

除了 setObject:@ 0 ,这开始看起来像你的例子。我还没有看到任何重构的东西。 (实际上有,但直到后来才注意到。让我们继续。)

Except for that setObject:@0, this is starting to look like your example. I don't see anything to refactor, yet. (There actually is, but I didn't notice until later. Let's keep going.)

现在我们可以再建一个测试:给定相同的条件,它应该在用户默认值中保存新的提醒ID。这可以通过复制早期测试,改变它并给它一个好名字来快速完成:

Now we can establish one more test: given those same conditions, it should save the new reminder ID in user defaults. This is quickly done by copying the earlier test, altering it, and giving it a good name:

- (void)testNextReminderIdWithCurrentReminderIdInUserDefaultsShouldSaveOneGreaterInUserDefaults
{
    // given
    [self setUpUserDefaultsWithCurrentReminderId:@3];

    // when
    [sut nextReminderId];

    // then
    [verify(mockUserDefaults) setObject:@4 forKey:@"currentReminderId"];
}

该测试失败,预期1匹配调用,但收到0。当然,为了让它通过,我们只需将 setObject:@ 0 更改为 setObject:reminderId 。一切都过去了。我们已经完成了!

That test fails, with "Expected 1 matching invocation, but received 0". To get it passing, of course, we simply change the setObject:@0 to setObject:reminderId. Everything passes. We're done!

等等,我们还没完成。第3步:有什么可以重构的吗?当我第一次写这篇文章时,我说,不是真的。但是看了清洁代码第3集之后再看看,我能听见鲍勃叔叔告诉我,一个功能应该有多大?4条线路可以,也许是5条线路......好吧.10条太大了。这是7行。我错过了什么?它必须通过做多件事来违反功能规则。

Wait, we're not done. Step 3: Is there anything to refactor? When I first wrote this, I said, "Not really." But looking it over after watching Clean Code episode 3, I can hear Uncle Bob telling me, "How big should a function be? 4 lines is OK, maybe 5. 6 is… OK. 10 is way too big." That's at 7 lines. What did I miss? It must be violating the rule of functions by doing more than one thing.

再一次,鲍勃叔叔:真正确定一个功能做一件事的唯一方法就是提取直到你掉落。前4行一起工作;他们计算实际价值。让我们选择它们,然后重构▶提取。遵循Bob叔叔第2集的范围规则,我们将给它一个很好的,长描述性的名称,因为它的使用范围非常有限。以下是自动重构为我们提供的信息:

Again, Uncle Bob: "The only way to be really be sure that a function does one thing is to extract 'til you drop." Those first 4 lines work together; they calculate the actual value. Let's select them, and Refactor ▶ Extract. Following Uncle Bob's scoping rule from episode 2, we'll give it a nice, long descriptive name since its scope of use is very limited. Here's what the automated refactoring gives us:

- (NSNumber *)determineNextReminderIdFromUserDefaults
{
    NSNumber *reminderId = [_userDefaults objectForKey:@"currentReminderId"];
    if (reminderId)
        reminderId = @([reminderId integerValue] + 1);
    else
        reminderId = @0;
    return reminderId;
}

- (NSNumber *)nextReminderId
{
    NSNumber *reminderId;
    reminderId = [self determineNextReminderIdFromUserDefaults];
    [_userDefaults setObject:reminderId forKey:@"currentReminderId"];
    return reminderId;
}

让我们清理它以使其变得更紧:

Let's clean that up to make it tighter:

- (NSNumber *)determineNextReminderIdFromUserDefaults
{
    NSNumber *reminderId = [_userDefaults objectForKey:@"currentReminderId"];
    if (reminderId)
        return @([reminderId integerValue] + 1);
    else
        return @0;
}

- (NSNumber *)nextReminderId
{
    NSNumber *reminderId = [self determineNextReminderIdFromUserDefaults];
    [_userDefaults setObject:reminderId forKey:@"currentReminderId"];
    return reminderId;
}

现在每个方法都很紧,任何人都可以轻松阅读3主要方法的行,看看它做了什么。但是,如果用户默认密钥分布在两种方法中,我会感到很不舒服。让我们将它提取到Example.m头部的常量中:

Now each method is really tight, and it's easy for anyone to read the 3 lines of the main method to see what it does. But I'm uncomfortable having that user defaults key spread across two methods. Let's extract that into a constant at the head of Example.m:

static NSString *const currentReminderIdKey = @"currentReminderId";

我将在生产代码中出现该密钥的任何地方使用该常量。但测试代码继续使用文字。这可以防止我们意外更改该常量键。

I'll use that constant wherever that key appears in the production code. But the test code continues to use the literals. This guards us from someone accidentally changing that constant key.

所以你有它。在五个测试中,我已经通过TDD找到了您要求的代码。希望它能让您更清楚地了解如何进行TDD,以及为什么它值得。通过以下三步华尔兹

So there you have it. In five tests, I have TDD'd my way to the code you asked for. Hopefully it gives you a clearer idea of how to TDD, and why it's worth it. By following the 3-step waltz


  1. 添加一个失败的测试

  2. 编写通过的最简单的代码,即使它看起来很笨

  3. 重构(生产代码和测试代码)

你不要只是在同一个地方。您最终得到:

you don't just end up at the same place. You end up with:


  • 支持依赖注入的完全隔离的代码,

  • 极简主义代码只实现已经过测试的内容,

  • 测试每个案例(测试本身已经过验证),

  • squeaky-clean代码,小巧,简单 - 读取方法。

  • well-isolated code that supports dependency injection,
  • minimalist code that only implements what has been tested,
  • tests for each case (with the tests themselves verified),
  • squeaky-clean code with small, easy-to-read methods.

所有这些好处将比投入TDD的时间节省更多时间 - 而不仅仅是长期,但是立即。

All these benefits will save more time than the time invested in TDD — and not just in the long term, but immediately.

有关完整应用程序的示例,请阅读测试驱动的iOS开发一书。这是我对该书的评论

For an example involving a full app, get the book Test-Driven iOS Development. Here's my review of the book.

这篇关于使用OCUnit的单元测试示例的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆