Как заглушить метод класса в OCMock?

Я часто нахожу в своих модульных тестах iPhone Objective-C, что я хочу, чтобы выровнял метод класса, например. NSUrlConnection + sendSynchronousRequest: returnResponse: error: метод.

Упрощенный пример:

- (void)testClassMock
{
    id mock = [OCMockObject mockForClass:[NSURLConnection class]];
    [[[mock stub] andReturn:nil] sendSynchronousRequest:nil returningResponse:nil error:nil];
}

При выполнении этого я получаю:

Test Case '-[WorklistTest testClassMock]' started.
Unknown.m:0: error: -[WorklistTest testClassMock] : *** -[NSProxy doesNotRecognizeSelector:sendSynchronousRequest:returningResponse:error:] called!
Test Case '-[WorklistTest testClassMock]' failed (0.000 seconds).

Мне очень трудно найти документацию по этому вопросу, но я предполагаю, что методы класса не поддерживаются OCMock.

Я нашел этот совет после многих Googling. Он работает, но очень громоздкий: http://thom.org.uk/2009/05/09/mocking-class-methods-in-objective-c/

Есть ли способ сделать это в OCMock? Или кто-то может подумать о умном объекте категории OCMock, который можно было бы написать для выполнения такого рода вещей?

Ответ 1

Исходя из мира Ruby, я точно понимаю, чего вы пытаетесь достичь. Видимо, вы буквально на три часа опережали меня, пытаясь сделать то же самое сегодня (часовая зона?: -).

Во всяком случае, я считаю, что это не поддерживается в том, как хотелось бы в OCMock, потому что укорачивание метода класса должно буквально дойти до класса и изменить его реализацию метода независимо от того, когда, где или кто вызывает этот метод. Это контрастирует с тем, что, по-видимому, делает OCMock, который должен предоставить вам прокси-объект, который вы манипулируете и иным образом управляете напрямую и вместо "реального" объекта указанного класса.

Например, кажется разумным хотеть заглушить NSURLConnection + sendSynchronousRequest: returnResponse: error: method. Тем не менее, типично, что использование этого вызова внутри нашего кода несколько зарыто, что делает его очень неудобным для параметризации и обмена в макет-объекте для класса NSURLConnection.

По этой причине, я думаю, что метод "swizzling", который вы обнаружили, а не сексуальный, - это именно то, что вы хотите сделать для методов класса stubbing. Сказать, что это очень громоздко кажется экстремальным - как насчет того, что мы согласны с этим "неэлегантным" и, возможно, не так удобно, как OCMock делает жизнь для нас. Тем не менее, это довольно сжатое решение проблемы.

Ответ 2

Обновление для OCMock 3

OCMock модернизировал свой синтаксис для поддержки stubbing метода класса:

id classMock = OCMClassMock([SomeClass class]);
OCMStub(ClassMethod([classMock aMethod])).andReturn(aValue);

Обновление

OCMock теперь поддерживает разметку метода класса из коробки. Теперь код OP должен работать как опубликованный. Если существует метод экземпляра с тем же именем, что и метод класса, синтаксис следующий:

[[[[mock stub] classMethod] andReturn:aValue] aMethod]

См. Возможности OCMock.

Оригинальный ответ

Пример кода, следующего за ответом Барри Варка.

Поддельный класс, просто обрезающий connectionWithRequest: delegate:

@interface FakeNSURLConnection : NSURLConnection
+ (id)sharedInstance;
+ (void)setSharedInstance:(id)sharedInstance;
+ (NSURLConnection *)connectionWithRequest:(NSURLRequest *)request delegate:(id<NSURLConnectionDelegate>)delegate;
- (NSURLConnection *)connectionWithRequest:(NSURLRequest *)request delegate:(id<NSURLConnectionDelegate>)delegate;
@end
@implementation FakeNSURLConnection
static id _sharedInstance;
+ (id)sharedInstance { if (!_sharedInstance) { _sharedInstance = [self init]; } return _sharedInstance; }
+ (void)setSharedInstance:(id)sharedInstance { _sharedInstance = sharedInstance; }
+ (NSURLConnection *)connectionWithRequest:(NSURLRequest *)request delegate:(id<NSURLConnectionDelegate>)delegate {
    return [FakeNSURLConnection.sharedInstance connectionWithRequest:request delegate:delegate];
}
- (NSURLConnection *)connectionWithRequest:(NSURLRequest *)request delegate:(id<NSURLConnectionDelegate>)delegate { return nil; }
@end

Переключение между макетами и из них:

{
    ...
    // Create the mock and swap it in
    id nsurlConnectionMock = [OCMockObject niceMockForClass:FakeNSURLConnection.class];
    [FakeNSURLConnection setSharedInstance:nsurlConnectionMock];
    Method urlOriginalMethod = class_getClassMethod(NSURLConnection.class, @selector(connectionWithRequest:delegate:));
    Method urlNewMethod = class_getClassMethod(FakeNSURLConnection.class, @selector(connectionWithRequest:delegate:));
    method_exchangeImplementations(urlOriginalMethod, urlNewMethod);

    [[nsurlConnectionMock expect] connectionWithRequest:OCMOCK_ANY delegate:OCMOCK_ANY];

    ...
    // Make the call which will do the connectionWithRequest:delegate call
    ...

    // Verify
    [nsurlConnectionMock verify];

    // Unmock
    method_exchangeImplementations(urlNewMethod, urlOriginalMethod);
}

Ответ 4

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

Ответ 5

Ссылка на blogpost в вопросе и RefuX gist вдохновила меня на то, чтобы придумать блокировку реализации своих идей: https://gist.github.com/1038034