Как заглушить метод класса в 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);
}
Ответ 3
Вот хороший "gist" с реализацией swizzle для методов класса: https://gist.github.com/314009
Ответ 4
Если вы измените свой тестируемый метод, чтобы принять параметр, который вводит класс NSURLConnection
, тогда относительно легко передать макет, который отвечает данному селектору (возможно, вам придется создать фиктивный класс в вашем тестовый модуль, который имеет селектор как метод экземпляра и издевается над этим классом). Без этой инъекции вы используете метод класса, по существу используя NSURLConnection
(класс) в качестве одноэлементного и, следовательно, попали в анти-шаблон с использованием одноэлементных объектов и пострадали тестируемость вашего кода.
Ответ 5
Ссылка на blogpost в вопросе и RefuX gist вдохновила меня на то, чтобы придумать блокировку реализации своих идей: https://gist.github.com/1038034