Пример модульного тестирования с OCUnit

Я действительно изо всех сил пытаюсь понять модульное тестирование. Я понимаю важность TDD, но все примеры модульного тестирования, о которых я читал, кажутся чрезвычайно простыми и тривиальными. Например, тестирование, чтобы убедиться, что свойство задано или если память выделена массиву. Почему? Если я закодирую ..alloc] init], Мне действительно нужно убедиться, что он работает?

Я новичок в разработке, поэтому я уверен, что мне чего-то не хватает здесь, особенно со всем увлечением, окружающим TDD.

I думаю, моя главная проблема в том, что я не могу найти практических примеров. Вот метод setReminderId это, кажется, хороший кандидат для тестирования. Как будет выглядеть полезный модульный тест, чтобы убедиться, что это работает? (используя 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;
}

1 ответов


Update: я улучшил этот ответ двумя способами: теперь это скринкаст, и я переключился с инъекции свойств на инъекцию конструктора. См.как начать работу с Objective-C TDD

хитрая часть заключается в том, что метод имеет зависимость от внешнего объекта NSUserDefaults. Мы не хотим использовать NSUserDefaults напрямую. Вместо этого нам нужно каким-то образом ввести эту зависимость, чтобы мы могли заменить поддельного пользователя по умолчанию для тестирования.

есть несколько различных способов сделать это. Один из них-передать его в качестве дополнительного аргумента методу. Другой-сделать его переменной экземпляра класса. И есть разные способы настроить этого Ивара. Существует "инъекция конструктора", где она указана в аргументах инициализатора. Или "инъекция собственности"."Для стандартных объектов из iOS SDK я предпочитаю сделать его свойством со значением по умолчанию.

так начнем с теста, что свойство по умолчанию является NSUserDefaults. Кстати, мой набор инструментов-это встроенный OCUnit Xcode, plus OCHamcrest для утверждения и OCMockito для макетов объектов. Есть и другие варианты, но это то, что я использую.

Первый Тест: Пользовательские Значения По Умолчанию

из-за отсутствия лучшего наименования, класс будет называться Example. Экземпляр будет называться sut для " тестируемой системы."Свойство . Вот первый тест, чтобы установить, каким должно быть его значение по умолчанию, в ExampleTests.м:

#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

на этом этапе это не компилируется, что считается ошибкой теста. Посмотри на него. Если вы можете заставить свои глаза пропустить скобки и скобки, тест должен быть довольно ясным.

давайте напишем самый простой код, который мы можем получить, чтобы этот тест компилировался и запускался - и терпел неудачу. Вот пример.h:

#import <Foundation/Foundation.h>

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

и впечатляющие Образец.м:

#import "Example.h"

@implementation Example
@end

нам нужно добавить строку в самое начало ExampleTests.м:

#import "Example.h"

тест выполняется и завершается с сообщением "ожидался экземпляр NSUserDefaults, но был равен нулю". Именно то, что мы хотели. Мы достигли этапа 1 нашего первого теста.

Шаг 2-написать простейший код, который мы можем пройти этот тест. Как насчет этого:

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

она проходит! Шаг 2 завершен.

Шаг 3-оптимизация код для включения всех изменений как в производственный код, так и в тестовый код. Но там пока нечего убирать. Мы закончили наше первое испытание. Что у нас пока есть? Начало класса, который может получить доступ NSUserDefaults, но также переопределите его для тестирования.

второй тест: без соответствующего ключа, возврат 0

теперь давайте напишем тест для метода. Чего мы от него хотим? Если у пользователя по умолчанию нет соответствующего ключа, мы хотим его вернуть 0.

при первом запуске с mock объектов, я рекомендую делать их вручную сначала, так что вы получите представление о том, что они. Затем начните использовать макет Object framework. Но я собираюсь прыгнуть вперед и использовать OCMockito, чтобы сделать вещи быстрее. Мы добавляем эти строки в ExampleTest.м:

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

по умолчанию макет объекта на основе OCMockito возвращает nil для любого метода. Но я напишу дополнительный код, чтобы сделать ожидание явным, сказав: "учитывая, что это попросил objectForKey:@"currentReminderId", он вернется nil."И учитывая все это, мы хотим, чтобы метод вернул NSNumber 0. (Я не собираюсь спорить, потому что я не знаю, для чего это. И я собираюсь назвать метод 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)));
}

это еще не компиляции. Давайте определим nextReminderId метод в Примере.h:

- (NSNumber *)nextReminderId;

и вот первая реализация в пример.м. Я хочу, чтобы тест провалился, поэтому я собираюсь вернуть поддельный номер:

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

тест завершается с сообщением "ожидалось , но было ". Важно, чтобы тест провалился, потому что это наш способ тестирования теста и обеспечения того, чтобы код, который мы пишем, переворачивал его из состояния сбоя в состояние прохождения. Шаг 1 завершен.

Шаг 2: Давайте пройдем тестовый тест. Но помните, нам нужен самый простой код, который пройдет тест. Это будет выглядеть ужасно глупо.

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

удивительно, это проходит! Но мы этот тест еще не закончен. Теперь мы подходим к шагу 3: рефакторинг. В тестах есть дубликат кода. Давайте тянуть sut, тестируемая система, в Ивар. Мы будем использовать -setUp метод для его настройки и -tearDown чтобы очистить его (уничтожить его).

@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

мы снова проводим тесты, чтобы убедиться, что они все еще проходят, и они проходят. Рефакторинг должен выполняться только в "зеленом" или проходящем состоянии. Все тесты должны продолжать проходить, независимо от того, выполняется ли рефакторинг в тестовом коде или рабочий код.

третий тест: без соответствующего ключа сохраните 0 в пользовательских значениях по умолчанию

теперь давайте проверим другое требование: пользовательские значения по умолчанию должны быть сохранены. Мы будем использовать те же условия, что и в предыдущем тесте. Но мы создаем новый тест, вместо того, чтобы добавлять больше утверждений к существующему тесту. В идеале каждый тест должен проверять одну вещь и иметь хорошее имя, чтобы соответствовать.

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

на verify заявление-это способ OCMockito сказать :" этот макет объекта когда-то меня должны были так называть."Мы запускаем тесты и получаем сбой, "ожидался 1 соответствующий вызов, но получил 0". Шаг 1 завершен.

Шаг 2: Самый простой код, который проходит. Готовы? Вот:

- (NSNumber *)nextReminderId
{
    [_userDefaults setObject:@0 forKey:@"currentReminderId"];
    return @0;
}
в пользовательских значениях по умолчанию вместо переменной с этим значением?- ты спрашиваешь. Потому что это все, что мы проверили. Держись, мы доберемся туда.

Шаг 3: оптимизация. Опять же, у нас есть дубликат кода в тесты. Давайте вытащим mockUserDefaults как Ивар.

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

тестовый код показывает предупреждения: "локальное объявление' mockUserDefaults 'скрывает переменную экземпляра". Исправьте их, чтобы использовать Ивар. Затем давайте извлекем вспомогательный метод, чтобы установить условие пользователя по умолчанию в начале каждого теста. Давайте потянем это nil в отдельную переменную, чтобы помочь нам с рефакторингом:

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

теперь выберите последние 3 строки, контекстный щелчок и выберите рефакторинг ▶ извлечь. Мы сделаем новый метод под названием setUpUserDefaultsWithCurrentReminderId:

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

тестовый код, который вызывает это теперь выглядит так:

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

единственной причиной этой переменной было помочь нам с автоматическим рефакторингом. Давайте встроим его:

    [self setUpUserDefaultsWithCurrentReminderId:nil];

тесты все еще проходят. Поскольку автоматический рефакторинг Xcode не заменил все экземпляры этого кода вызовом нового вспомогательного метода, мы должны сделать это сами. Итак, теперь тесты выглядят так это:

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

видите, как мы постоянно чистим, когда идем? Тесты стали легче читать!

четвертый тест: с соответствующим ключом, возвращаемое увеличенное значение

теперь мы хотим проверить, что если пользователь по умолчанию имеет некоторое значение, мы возвращаем один больше. Я собираюсь скопировать и изменить тест "должен вернуть ноль", используя произвольное значение 3.

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

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

это не удается, как и ожидалось: "ожидалось , но было ".

вот простой код для прохождения теста:

- (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, это начинает выглядеть как ваш пример. Я пока ничего не вижу для рефакторинга. (На самом деле есть, но я заметила позже. Давайте продолжим.)

пятый тест: с соответствующим ключом, хранить увеличенное значение

теперь мы можем установить еще один тест: учитывая те же условия, он должен сохранить новый идентификатор напоминания в настройках пользователя. Это быстро сделано, скопировав более ранний тест, изменив его и дав ему хорошее имя:

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

    // when
    [sut nextReminderId];

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

этот тест завершается неудачей, с "ожидаемым 1 совпадающим вызовом, но полученным 0". Чтобы получить его прохождение, конечно, мы просто меняем setObject:@0 to setObject:reminderId. Все проходит. Мы закончили!

Подожди, мы еще не закончили. Шаг 3: Есть ли что-нибудь для рефакторинга? Когда я впервые написал это, я сказал: "Не совсем.- Но осматривать его после просмотра!--144-- > Чистый код Эпизод 3, I слышу, как дядя Боб говорит мне: "насколько большой должна быть функция? 4 строки в порядке, может быть, 5. 6-это... нормально. 10 - это слишком много."Это на 7 линии. Что я пропустил? Он должен нарушать правило функций, делая более одной вещи.

  1. добавить один неудачный тест
  2. напишите Самый простой код, который проходит, даже если он выглядит тупым
  3. Рефактор (как производственный код, так и тестовый код)

вы не просто в конечном итоге в том же месте. Вы в конечном итоге:

  • хорошо изолированный код, поддерживающий инъекцию зависимостей,
  • минималистический код, который реализует только то, что было испытано,
  • тесты для каждого случая (с проверенными тестами),
  • скрипучий-чистый код с небольшими, легко читаемыми методами.

все эти преимущества сэкономят больше времени, чем время, вложенное в TDD - и не только в долгосрочной перспективе, но и сразу.

для примера с участием полного приложения, получить книгу тестовая разработка iOS. Вот!--184-->мой обзор книги.