Пример модульного тестирования с 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 линии. Что я пропустил? Он должен нарушать правило функций, делая более одной вещи.
- добавить один неудачный тест
- напишите Самый простой код, который проходит, даже если он выглядит тупым
- Рефактор (как производственный код, так и тестовый код)
вы не просто в конечном итоге в том же месте. Вы в конечном итоге:
- хорошо изолированный код, поддерживающий инъекцию зависимостей,
- минималистический код, который реализует только то, что было испытано,
- тесты для каждого случая (с проверенными тестами),
- скрипучий-чистый код с небольшими, легко читаемыми методами.
все эти преимущества сэкономят больше времени, чем время, вложенное в TDD - и не только в долгосрочной перспективе, но и сразу.
для примера с участием полного приложения, получить книгу тестовая разработка iOS. Вот!--184-->мой обзор книги.