2014-01-07 6 views
10

Я использую XCTest и OCMock для написания модульных тестов для приложения iOS, и мне нужно руководствоваться тем, как наилучшим образом разработать единичный тест, который проверяет, что метод приводит к тому, что метод NSTimer начал.Запуск модульного теста для проверки запуска NSTimer

Код тестируемой:

- (void)start { 
    ... 
    self.timer = [NSTimer timerWithTimeInterval:1.0 
             target:self 
             selector:@selector(tick:) 
             userInfo:nil 
             repeats:YES]; 
    NSRunLoop *runLoop = [NSRunLoop currentRunLoop]; 
    [runLoop addTimer:self.timer forMode:NSDefaultRunLoopMode]; 
    ... 
} 

То, что я хочу, чтобы проверить, что таймер создается с правильными аргументами, и что таймер планируется запустить на цикле выполнения.

Я думал о следующих вариантов, что я не доволен:

  1. На самом деле ждать таймер стрелять. (Причина мне не нравится: ужасная практика тестирования модулей. Это больше похоже на медленный интеграционный тест.)
  2. Извлеките стартовый код таймера в частный метод, разоблачите этот частный метод в файле расширения класса на модульные тесты, и использовать макетное ожидание, чтобы проверить, вызван ли частный метод. (Причина мне не нравится: он проверяет, что метод вызван, но не тот метод, который фактически устанавливает таймер для правильной работы. Кроме того, разоблачение частных методов не является хорошей практикой.)
  3. Предоставить макет NSTimer к тестируемому коду. (Причина, по которой мне это не нравится: не удается проверить, что на самом деле он запланирован для запуска, потому что таймеры запускаются через цикл запуска, а не с некоторого метода запуска NSTimer.)
  4. Предоставьте макет NSRunLoop и убедитесь, что addTimer:forMode: получает называется. (Причина мне не нравится: я должен был бы обеспечить интерфейс в цикле запуска? Это кажется дурацким.)

Может ли кто-нибудь предоставить некоторый юнит-тестирование коучинга?

+0

Вам не нужно проверять, что он _actually получает schedule_ для запуска. Биты планирования и запуска не являются вашим кодом; даже если вы их протестировали, и они не сработали, что вы можете с этим поделать? Вам просто нужно убедиться, что ваш код делает то, что ему нужно сделать до этого момента, правильно взаимодействует с каркасом. Поэтому я думаю, что номер три - это ответ - скажите, что из класса класса «NSTimer» следует ожидать «timerWithTimeInterval: ...» –

+0

Смысл 'NSRunLoop' также имеет смысл; У Майка Эша есть статья, которая, вероятно, будет полезна там: https://www.mikeash.com/pyblog/friday-qa-2010-01-01-nsrunloop-internals.html –

+0

Джош, спасибо за руководство! Я больше думал об этой проблеме, и ваш комментарий к «планированию и запуску битов не является вашим кодом» напомнил мне, что я где-то читал «Не тестируйте реализацию Apple». Это не имело смысла до сих пор. Я пытался разработать свой тест, чтобы охватить все возможные сценарии, но вы просто не можете этого сделать, когда вы используете чей-то код для работы, все, что вы можете сделать, это проверить, что происходит взаимодействие. –

ответ

6

Хорошо! Понадобилось время, чтобы понять, как это сделать. Я полностью объясню свой мыслительный процесс. Извините за долговечность.

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

Итак, как вы пишете единичный тест, чтобы убедиться, что ваш код запускает повторяющийся таймер правильно? Есть три вещи, которые вы можете проверить в модульном тесте:

  • Возвращаемое значение метода
  • Изменение состояния или поведения системы (предпочтительно доступен через открытый интерфейс)
  • взаимодействия ваш код с другим кодом, который вы не контролируете

NSTimer с и NSRunLoop, я должен был проверить для взаимодействия, потому что нет никакого способа, чтобы убедиться, что внешне таймер был настроен правильно. Серьезно, нет repeats собственности. Вы должны перехватить вызов метода, который создает сам таймер.

Далее я понял, что мне не нужно было касаться NSRunLoop, если бы я создал таймер с +scheduledTimerWithTimeInterval:target:selector:userInfo:repeats, который автоматически запускает таймер.Это одно меньшее взаимодействие, которое я должен проверить.

И, наконец, чтобы создать ожидание того, что вызывается +scheduledTimerWithTimeInterval:target:selector:userInfo:repeats, вы должны высмеять класс NSTimer, который, к счастью, может сделать OCMock. Вот что тест был похож:

id mockTimer = [OCMockObject mockForClass:[NSTimer class]]; 
[[mockTimer expect] scheduledTimerWithTimeInterval:1.0 
              target:[OCMArg any] 
              selector:[OCMArg anySelector] 
              userInfo:[OCMArg any] 
              repeats:YES]; 

<your code that should create/schedule NSTimer> 

[mockTimer verify]; 

Глядя на этот тест, я подумал: «Подождите, как вы можете проверить, что таймер настроен на правильную цель и селектор?» Ну, я, наконец, понял, что мне на самом деле не важно, что он настроен с определенной целью и селектором, мне все равно, что когда срабатывает таймер, он делает то, что мне нужно. И это действительно важный момент для написания хороших, будущих тестов: действительно старайтесь не полагаться на частный интерфейс или детали реализации, потому что эти вещи меняются. Вместо этого проверьте поведение своего кода, которое не изменится, и сделайте это через открытый интерфейс.

Это приводит нас ко второму модулю: таймер делает то, что мне нужно? Чтобы проверить это, к счастью NSTimer имеет -fire, что заставляет таймер выполнять селектор цели. Таким образом, вам не нужно даже создавать поддельные NSTimer или сделать выписку & переопределение для создания пользовательского макета таймера, все, что вам нужно сделать, это пусть рип:

id mockObserver = [OCMockObject observerMock]; 
[[NSNotificationCenter defaultCenter] addMockObserver:mockObserver 
               name:@"SomeNotificationName" 
               object:nil]; 
[[mockObserver expect] notificationWithName:@"SomeNotificationName" 
            object:[OCMArg any]]; 
[myCode startTimer]; 

[myCode.timer fire]; 

[mockObserver verify]; 
[[NSNotificationCenter defaultCenter] removeObserver:mockObserver]; 

Несколько замечаний по поводу этого теста :

  • Когда срабатывает таймер, тест ожидает, что NSNotification размещена по умолчанию NSNotificationCenter. OCMock удалось не разочаровать: широковещательное тестирование уведомлений - это просто.
  • Чтобы запустить огонь по таймеру, вам нужна ссылка на таймер. Мой тестируемый класс не раскрывает NSTimer в открытом интерфейсе, поэтому я сделал это, создав расширение класса, которое обнаружило свойство private NSTimer для моих тестов, as described in this SO post.
+0

Я кое-что из новичка TDD, поэтому может быть что-то, что я пропускаю, но я не уверен, что ваш тест что-то делает. Конечно, когда вы запускаете его, вы получаете зеленый знак, но в основном вы подтвердили, что NSTimer действительно работает так, как рекламируется. Это полезно, когда вы тестируете Apple, но что это говорит о вашем коде? Насколько я вижу, ничего.В вашем приложении вы можете пропустить селектор таймера, указать неправильную цель, запустить таймер в неподходящий момент и забыть его недействительным, но согласно вышеприведенному тесту вы отлично справляетесь. –

+0

Точка теста - это не проверка того, что NSTimer делает то, что он делает, но когда он делает то, что он делает, ваш тестируемый код (CUT) делает все правильно. В модульном тесте * good * вы хотите проверить, что он делает правильную * общедоступную * вещь - мутировать общедоступную переменную, изменять ее состояние публично заметным образом, вызывать метод на стороннем объекте, возвращать право значение или в этом случае транслировать правильное уведомление. –

+0

Вы можете ошибочно установить селектор таймера, но если вы это сделали, он не будет транслировать уведомление. Или вы можете планировать его запуск каждые 10 секунд, а не каждую секунду, но именно поэтому я настроил таймер макета, чтобы ожидать, что он получит schedTimerWithTimeInterval: 1.0. В конечном счете, модульные тесты могут и не будут обеспечивать 100% уверенность в том, что вы все сделали правильно. Я имею в виду, даже ожидая scheduleTimerWithTimeInterval: это довольно хрупкий и хрупкий модульный тест, потому что кто-то может создать таймер с другим инициализатором. Вот где интеграционные/функциональные тесты должны заполнить отверстия –

1

Мне очень понравился первый подход Ричарда, я немного расширил код, чтобы использовать вызовы блоков, чтобы избежать необходимости ссылаться на приватное свойство NSTimer.

[[mockAPIClient expect] someMethodSuccess:[OCMIsNotNilConstraint constraint] 
            failure:[OCMIsNotNilConstraint constraint]; 

id mockTimer = [OCMockObject mockForClass:[NSTimer class]]; 
[[[mockTimer expect] andDo:^(NSInvocation *invocation) { 
    SEL selector = nil; 
    [invocation getArgument:&selector atIndex:4]; 
    [testSubject performSelector:selector]; 
}] scheduledTimerWithTimeInterval:10.0 
          target:testSubject 
         selector:[OCMArg anySelector] 
         userInfo:nil 
          repeats:YES]; 

[testSubject viewWillAppear:YES]; 

[mockTimer verify]; 
[mockAPIClient verify]; 
+0

Nice! Не понимал, что вы можете схватить аргументы, отправленные таким образом. Это улучшение, которое не нужно копаться в частной реализации класса. Я действительно думаю, что это связано с читабельностью теста, но, как и большинство вещей, это компромисс. –

Смежные вопросы