#ios #objective-c #ocmock #dispatch-async
#iOS #objective-c #ocmock #отправка-асинхронная
Вопрос:
У меня есть метод на моем контроллере просмотра, который использует dispatch_async. Через некоторое время он вызывает другой метод. В моем тесте я хочу убедиться, что вызывается метод followup.
Похоже, что большинство людей советуют при работе с OCMock и dispatch_async использовать XCTestExpectation и вызывать fulfilly после завершения задачи. Однако в моем тесте у меня нет способа узнать, когда задача завершена, поскольку функция не имеет обратного вызова. В результате тест завершается до завершения задачи, и проверка завершается неудачей.
Вот минимально воспроизводимый пример, демонстрирующий мою проблему:
Просмотр контроллера
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
}
- (void)usesAsyncQueue{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:5.0]]; //long running task, e.g. network request
dispatch_async(dispatch_get_main_queue(), ^{
[self usedInAsyncQueue];
});
});
}
- (void)usedInAsyncQueue{
// we want to verify that this is called
}
@end
Тест
@implementation ViewControllerTest
- (void)testUsesAsyncQueue {
ViewController * testViewController = [[ViewController alloc] init];
id viewControllerMock = OCMPartialMock(testViewController);
OCMExpect([viewControllerMock usedInAsyncQueue]);
[testViewController usesAsyncQueue];
[[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:5.1]]; //comment this out and the test fails
OCMVerify([viewControllerMock usedInAsyncQueue]);
}
@end
Как вы можете видеть в моем тесте, если я добавлю команду sleep в тест, код будет работать нормально. Однако у меня нет способа узнать, как долго будет длиться задержка, и я не хочу устанавливать для нее безопасный промежуток времени, если на самом деле он был бы короче. Я не хочу устанавливать задержку на 5 секунд, если иногда это займет всего 0,2 секунды.
Есть ли способ перехватить вызов usedInAsyncQueue
, когда это произойдет, вместо того, чтобы ждать некоторое время, а затем проверять?
Комментарии:
1. Эти другие верны — вам нужно использовать XCTestExpectation с таймаутом и добавить fulfilly в свою функцию, именно там, где у вас есть,
// we want to verify...
вы можете поместить что-то вроде[expectation fulfill];
для этого2. Но эта строка находится в коде приложения, а не в коде тестирования. Как я могу вообще получить доступ к экземпляру XCExpectation там? И разве в лучших практиках не говорится, что вы не должны помещать тестовый код в свое приложение?
3. … и вам также нужно добавить
testUsedInAsyncQueue
в макет, и там вы добавляете выполнить
Ответ №1:
Возможно, проще всего, как показано ниже — тогда ожидание и его выполнение находятся в одном сообщении.
@implementation Test
- ( void ) testSomeFunc
{
XCTestExpectation * expectation = [[XCTestExpectation alloc] initWithDescription:@"Test"];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^ {
// Some long task
// Now done ...
dispatch_async(dispatch_get_main_queue(), ^{
// Note this fulfill happens in the test code
[expectation fulfill];
// This calls the original code, not the test code
[self usedInAsyncQueue];
});
});
// Wait for it ...
[self waitForExpectations:[NSArray arrayWithObject:expectation] timeout:10];
}
@end
В качестве альтернативы, что-то вроде этого, где вы выполняете внутри сообщения, которое хотите протестировать.
@interface Test
@property (nonatomic,strong) XCTextExpectation * expectation;
@end
@implementation Test
- ( void ) testSomeFunc
{
// Here you need some reference to the expectation as it is fulfilled inside the func itself later
self.expectation = [[XCTestExpectation alloc] initWithDescription:@"Test"];
// Some code that eventually calls testFunc async and in a block
// Note this calls *test*Func, it calls test code
...
// Wait for it as before
}
// This is a test func
- ( void ) testFunc
{
// Here the fulfill takes place inside test
[self.expectation fulfill];
// rest of func
// here you call the code itself, not the test...
}
@end
ПРАВКА 4
Вот еще одна альтернатива, использующая параметры блока и ваш код более или менее как есть.
Просмотр контроллера
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
}
// Using a completion block here specifically for testing
// Call this with nil when not testing
- (void)usesAsyncQueue:( void (^)( void ) ) completion {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:5.0]]; //long running task, e.g. network request
dispatch_async(dispatch_get_main_queue(), ^{
if ( completion )
{
completion();
}
[self usedInAsyncQueue];
});
});
}
Тест
@implementation ViewControllerTest
- (void)testUsesAsyncQueue {
ViewController * testViewController = [[ViewController alloc] init];
id viewControllerMock = OCMPartialMock(testViewController);
XCTestExpectation * expectation = [[XCTestExpectation alloc] initWithDescription:@"Test"];
// Here we pass a block that will fulfill the expectation
[testViewController usesAsyncQueue:^ { expectation.fulfill; }];
// Wait for it ...
[self waitForExpectations:[NSArray arrayWithObject:expectation] timeout:10];
}
Комментарии:
1. Я думаю, что я, должно быть, что-то здесь упускаю. Что это должно быть тестированием?
2. Привет, Малахия — это должно подтвердить, что блок выполняется. Блок выполняется асинхронно, и, если я вас правильно понимаю, вам нужно проверить в своем тесте, что
usedInAsyncQueue
вызывается. Это «упрощение» делает именно это, оно соответствует ожиданиям при вызове этого сообщения.3. Это действительно просто показывает, куда поместить ожидание и где его выполнить. Но вы могли бы, например, сделать это по-другому и также поместить функцию выполнения в
testUsedInAsyncQueue
, но я думаю, что это проще всего, поскольку в конечном итоге вы используетеusedInAsyncQueue
вместо, а ожидание является локальным только для сообщения. Помогает ли это / имеет ли смысл?4. Метод, который я тестирую, использует asyncqueue. Я понимаю, что у вас здесь есть упрощение, но я не понимаю, как в ваших тестах кода используется Asyncqueue
5. Цель состоит в том, чтобы убедиться, что usedInAsyncQueue действительно используется при вызове метода usesAsyncQueue. Вызванный вручную usedInAsyncQueue, похоже, не достигает цели
Ответ №2:
Хитрость заключается в том, чтобы заглушить метод, который вы хотите проверить, и вызвать его fulfill
. В примере в OP вы должны заглушить метод usedInAsyncQueue
и вызвать его fulfill
. Затем вы можете дождаться его вызова и завершить тест по таймауту.
Вот полный пример теста
- (void)testUsesAsyncQueue {
ViewController * testViewController = [[ViewController alloc] init];
id viewControllerMock = OCMPartialMock(testViewController);
XCTestExpectation * expectation = [[XCTestExpectation alloc] initWithDescription:@"test"];
OCMStub([viewControllerMock usedInAsyncQueue]).andDo(^(NSInvocation* invocation){
[expectation fulfill];
});
OCMExpect([viewControllerMock usedInAsyncQueue]);
[testViewController usesAsyncQueue];
[self waitForExpectations:@[expectation] timeout:5.1];
OCMVerify([viewControllerMock usedInAsyncQueue]);
}
Я запустил этот пример программы, а также свой исходный рабочий код, и оба работают отлично