Блокировка для dispatch_async без обратного вызова

#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]);
}
  

Я запустил этот пример программы, а также свой исходный рабочий код, и оба работают отлично