Pregunta

A menudo encuentro en mis pruebas unitarias de iPhone Objective-C que quiero eliminar un método de clase, por ejemplo. NSUrlConnection's + sendSynchronousRequest: returnResponse: error: method.

Ejemplo simplificado:

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

Al ejecutar esto, me sale:

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).

Me ha costado mucho encontrar documentación sobre esto, pero asumo que los métodos de clase no son compatibles con OCMock.

He encontrado este consejo después de un montón de Google. Funciona, pero es muy engorroso: http://thom.org.uk/ 2009/05/09 / mocking-class-methods-in-object-c /

¿Hay alguna forma de hacer esto dentro de OCMock? ¿O alguien puede pensar en un objeto de categoría OCMock inteligente que podría escribirse para lograr este tipo de cosas?

¿Fue útil?

Solución

Viniendo del mundo de Ruby, entiendo exactamente lo que estás tratando de lograr. Aparentemente, estabas literalmente tres horas por delante de mí intentando hacer exactamente lo mismo hoy (¿cosa de la zona horaria? :-).

De todos modos, creo que esto no se admite de la manera que uno desearía en OCMock porque aplastar un método de clase necesita literalmente entrar en la clase y cambiar su implementación de método independientemente de cuándo, dónde, o quien llama al método. Esto contrasta con lo que parece hacer OCMock, que es proporcionarle un objeto proxy que manipula y de lo contrario opera directamente y en lugar de un " real " objeto de la clase especificada.

Por ejemplo, parece razonable querer anular NSURLConnection + sendSynchronousRequest: returnResponse: error: method. Sin embargo, es típico que el uso de esta llamada dentro de nuestro código esté algo enterrado, por lo que es muy difícil parametrizarlo e intercambiarlo en un objeto simulado para la clase NSURLConnection.

Por este motivo, creo que el método " swizzling " El enfoque que has descubierto, aunque no es sexy, es exactamente lo que quieres hacer para aplastar los métodos de clase. Decir que es muy engorroso parece extremo. ¿Qué tal si estamos de acuerdo? Es " Inelegant " Y tal vez no sea tan conveniente como OCMock nos hace la vida. Sin embargo, es una solución bastante concisa para el problema.

Otros consejos

Actualización para OCMock 3

OCMock ha modernizado su sintaxis para soportar el apéndice de métodos de clase:

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

Actualizar

OCMock ahora es compatible con el método de clase apaga fuera de la caja. El código del OP ahora debería funcionar como publicado. Si hay un método de instancia con el mismo nombre que el método de clase, la sintaxis es:

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

Consulte Funciones de OCMock .

Respuesta original

Código de muestra después de la respuesta de Barry Wark.

La clase falsa, simplemente apaleando 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

Cambio hacia y desde el simulacro:

{
    ...
    // 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);
}

Aquí hay un buen 'gist' con una implementación de swizzle para métodos de clase: https://gist.github.com / 314009

Si modifica su método bajo prueba para tomar un parámetro que inyecta la clase del NSURLConnection , entonces es relativamente fácil pasar un simulacro que responde al selector dado (es posible que tenga que crear una clase ficticia en su módulo de prueba que tiene el selector como método de instancia y simula esa clase). Sin esta inyección, está usando un método de clase, esencialmente usando NSURLConnection (la clase) como singleton y, por lo tanto, ha caído en el antipatrón de usar objetos singleton y la capacidad de prueba de su código ha sufrido. .

El enlace a la publicación del blog en la pregunta y RefuX me inspiró a encontrar la implementación de sus ideas habilitada por bloques: https : //gist.github.com/1038034

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top