unitarias tutorial pruebas objective integracion español ejemplo desventajas objective-c ios unit-testing tdd ocunit

objective c - tutorial - Ejemplo de prueba unitaria con OCUnit



pruebas unitarias php (1)

Realmente estoy luchando por comprender las pruebas unitarias. Entiendo la importancia de TDD, pero todos los ejemplos de pruebas unitarias que leí parecen ser extremadamente simples y triviales. Por ejemplo, probar para asegurarse de que se establece una propiedad o si la memoria está asignada a una matriz. ¿Por qué? Si codigo ... ..alloc] init] , ¿realmente necesito asegurarme de que funciona?

Soy nuevo en el desarrollo, así que estoy seguro de que me falta algo aquí, especialmente con toda la locura que rodea a TDD.

Creo que mi principal problema es que no puedo encontrar ningún ejemplo práctico. Aquí hay un método setReminderId que parece ser un buen candidato para las pruebas. ¿Cómo sería una prueba de unidad útil para asegurarse de que esto funciona? (usando OCUnit)

- (NSNumber *)setReminderId: (NSDictionary *)reminderData { NSNumber *currentReminderId = [[NSUserDefaults standardUserDefaults] objectForKey:@"currentReminderId"]; if (currentReminderId) { // Increment the last reminderId currentReminderId = @(currentReminderId.intValue + 1); } else { // Set to 0 if it doesn''t already exist currentReminderId = @0; } // Update currentReminderId to model [[NSUserDefaults standardUserDefaults] setObject:currentReminderId forKey:@"currentReminderId"]; return currentReminderId; }


Actualización: he mejorado esta respuesta de dos formas: ahora es un screencast, y cambié de inyección de propiedad a inyección de constructor. Vea cómo comenzar con Objective-C TDD

La parte difícil es que el método tiene una dependencia en un objeto externo, NSUserDefaults. No queremos usar NSUserDefaults directamente. En cambio, tenemos que inyectar esta dependencia de alguna manera, de modo que podamos sustituir los valores predeterminados de un usuario falso por las pruebas.

Hay algunas maneras diferentes de hacer esto. Una es pasarla como un argumento adicional al método. Otra es convertirla en una variable de instancia de la clase. Y hay diferentes formas de configurar este ivar. Hay una "inyección de constructor" donde se especifica en los argumentos del inicializador. O hay "inyección de propiedad". Para los objetos estándar del iOS SDK, mi preferencia es convertirlo en una propiedad, con un valor predeterminado.

Comencemos con una prueba de que la propiedad es, por defecto, NSUserDefaults. Por cierto, mi conjunto de herramientas es el OCUnit integrado de Xcode, más OCHamcrest para aserciones y OCMockito para objetos simulados. Hay otras opciones, pero eso es lo que uso.

Primera prueba: valores predeterminados del usuario

A falta de un nombre mejor, la clase se llamará Example . La instancia se denominará sut para "sistema bajo prueba". La propiedad se llamará userDefaults . Aquí hay una primera prueba para establecer cuál debería ser su valor predeterminado en ExampleTests.m:

#import <SenTestingKit/SenTestingKit.h> #define HC_SHORTHAND #import <OCHamcrestIOS/OCHamcrestIOS.h> @interface ExampleTests : SenTestCase @end @implementation ExampleTests - (void)testDefaultUserDefaultsShouldBeSet { Example *sut = [[Example alloc] init]; assertThat([sut userDefaults], is(instanceOf([NSUserDefaults class]))); } @end

En esta etapa, esto no se compila, lo que cuenta como el error de la prueba. Mirar por encima. Si puede lograr que sus ojos omitan los corchetes y paréntesis, la prueba debería ser bastante clara.

Escribamos el código más simple posible para hacer que la prueba se compile y se ejecute, y fallar. Aquí está el Ejemplo.h:

#import <Foundation/Foundation.h> @interface Example : NSObject @property (strong, nonatomic) NSUserDefaults *userDefaults; @end

Y el imponente Example.m:

#import "Example.h" @implementation Example @end

Necesitamos agregar una línea al principio de ExampleTests.m:

#import "Example.h"

La prueba se ejecuta y falla con el mensaje "Se esperaba una instancia de NSUserDefaults, pero era nula". Exactamente lo que queríamos Hemos llegado al paso 1 de nuestra primera prueba.

El paso 2 es escribir el código más simple que podamos para pasar esa prueba. Qué tal esto:

- (id)init { self = [super init]; if (self) _userDefaults = [NSUserDefaults standardUserDefaults]; return self; }

¡Pasó! El paso 2 está completo.

El paso 3 es refactorizar el código para incorporar todos los cambios, tanto en el código de producción como en el código de prueba. Pero realmente no hay nada que limpiar aún. Hemos terminado con nuestra primera prueba. ¿Qué tenemos hasta ahora? Los inicios de una clase que puede acceder a NSUserDefaults , pero también se puede anular para probar.

Segunda prueba: sin la tecla correspondiente, devuelve 0

Ahora vamos a escribir una prueba para el método. ¿Qué queremos que haga? Si el usuario predeterminado no tiene una clave coincidente, queremos que devuelva 0.

Cuando empiece por objetos simulados, recomiendo hacerlos a mano al principio, para que pueda hacerse una idea de para qué sirven. Luego comience a usar un marco de objeto simulado. Pero voy a adelantarme y usar OCMockito para hacer las cosas más rápido. Agregamos estas líneas a ExampleTest.m:

#define MOCKITO_SHORTHAND #import <OCMockitoIOS/OCMockitoIOS.h>

De forma predeterminada, un objeto simulado basado en OCMockito devolverá nil para cualquier método. Pero escribiré un código adicional para hacer explícita la expectativa diciendo: "dado que se le pide objectForKey:@"currentReminderId" , devolverá nil ." Y dado todo eso, queremos que el método devuelva NSNumber 0. (No voy a pasar un argumento, porque no sé para qué nextReminderId . Y voy a nombrar el método nextReminderId ).

- (void)testNextReminderIdWithNoCurrentReminderIdInUserDefaultsShouldReturnZero { Example *sut = [[Example alloc] init]; NSUserDefaults *mockUserDefaults = mock([NSUserDefaults class]); [sut setUserDefaults:mockUserDefaults]; [given([mockUserDefaults objectForKey:@"currentReminderId"]) willReturn:nil]; assertThat([sut nextReminderId], is(equalTo(@0))); }

Esto no se compila todavía. Definamos el método nextReminderId en Example.h:

- (NSNumber *)nextReminderId;

Y aquí está la primera implementación en Example.m. Quiero que la prueba falle, así que voy a devolver un número falso:

- (NSNumber *)nextReminderId { return @-1; }

La prueba falla con el mensaje "Esperado <0>, pero era <-1>". Es importante que la prueba falle, porque es nuestra forma de probar la prueba y garantizar que el código que escribimos la cambie de un estado fallido a uno que pase. El paso 1 está completo.

Paso 2: hagamos pasar la prueba de prueba. Pero recuerde, queremos el código más simple que pase la prueba. Se verá terriblemente tonto.

- (NSNumber *)nextReminderId { return @0; }

Increíble, pasa! Pero aún no hemos terminado con esta prueba. Ahora llegamos al Paso 3: refactor. Hay código duplicado en las pruebas. Llevemos sut , el sistema bajo prueba, a un ivar. Usaremos el método -setUp para configurarlo, y -tearDown para limpiarlo (destruyéndolo).

@interface ExampleTests : SenTestCase { Example *sut; } @end @implementation ExampleTests - (void)setUp { [super setUp]; sut = [[Example alloc] init]; } - (void)tearDown { sut = nil; [super tearDown]; } - (void)testDefaultUserDefaultsShouldBeSet { assertThat([sut userDefaults], is(instanceOf([NSUserDefaults class]))); } - (void)testNextReminderIdWithNoCurrentReminderIdInUserDefaultsShouldReturnZero { NSUserDefaults *mockUserDefaults = mock([NSUserDefaults class]); [sut setUserDefaults:mockUserDefaults]; [given([mockUserDefaults objectForKey:@"currentReminderId"]) willReturn:nil]; assertThat([sut nextReminderId], is(equalTo(@0))); } @end

Ejecutamos las pruebas nuevamente, para asegurarnos de que todavía pasen, y lo hacen. La refabricación solo debe hacerse en estado "verde" o pasable. Todas las pruebas deben continuar pasando, ya sea que la refactorización se realice en el código de prueba o en el código de producción.

Tercera prueba: sin la clave correspondiente, almacene 0 en los valores predeterminados del usuario

Ahora probemos otro requisito: los valores predeterminados del usuario deberían guardarse. Utilizaremos las mismas condiciones que la prueba anterior. Pero creamos una nueva prueba, en lugar de agregar más afirmaciones a la prueba existente. Idealmente, cada prueba debería verificar una cosa y tener un buen nombre para que coincida.

- (void)testNextReminderIdWithNoCurrentReminderIdInUserDefaultsShouldSaveZeroInUserDefaults { // given NSUserDefaults *mockUserDefaults = mock([NSUserDefaults class]); [sut setUserDefaults:mockUserDefaults]; [given([mockUserDefaults objectForKey:@"currentReminderId"]) willReturn:nil]; // when [sut nextReminderId]; // then [verify(mockUserDefaults) setObject:@0 forKey:@"currentReminderId"]; }

La declaración de verify es la forma de OCMockito de decir: "Este objeto falso debería haberse llamado así una vez". Ejecutamos las pruebas y obtenemos una falla, "Se esperaba una invocación de coincidencia, pero recibimos 0". El paso 1 está completo.

Paso 2: el código más simple que pasa. Listo? Aquí va:

- (NSNumber *)nextReminderId { [_userDefaults setObject:@0 forKey:@"currentReminderId"]; return @0; }

"¿Pero por qué estás guardando @0 en los valores predeterminados del usuario, en lugar de una variable con ese valor?" usted pregunta. Porque eso es lo que hemos probado. Espera, llegaremos allí.

Paso 3: refactor Nuevamente, tenemos código duplicado en las pruebas. Vamos a sacar mockUserDefaults como un ivar.

@interface ExampleTests : SenTestCase { Example *sut; NSUserDefaults *mockUserDefaults; } @end

El código de prueba muestra advertencias, "La declaración local de ''mockUserDefaults'' oculta la variable de instancia". Arreglelos para usar el ivar. Luego, vamos a extraer un método de ayuda para establecer la condición de los valores predeterminados del usuario al inicio de cada prueba. Vamos a sacar ese nil a una variable separada para ayudarnos con la refactorización:

NSNumber *current = nil; mockUserDefaults = mock([NSUserDefaults class]); [sut setUserDefaults:mockUserDefaults]; [given([mockUserDefaults objectForKey:@"currentReminderId"]) willReturn:current];

Ahora seleccione las últimas 3 líneas, haga clic en el contexto y seleccione Refactorizar ▶ Extraer. Crearemos un nuevo método llamado setUpUserDefaultsWithCurrentReminderId:

- (void)setUpUserDefaultsWithCurrentReminderId:(NSNumber *)current { mockUserDefaults = mock([NSUserDefaults class]); [sut setUserDefaults:mockUserDefaults]; [given([mockUserDefaults objectForKey:@"currentReminderId"]) willReturn:current]; }

El código de prueba que invoca esto ahora se ve así:

NSNumber *current = nil; [self setUpUserDefaultsWithCurrentReminderId:current];

La única razón para esa variable fue ayudarnos con la refactorización automatizada. Vamos a alinearlo:

[self setUpUserDefaultsWithCurrentReminderId:nil];

Las pruebas aún pasan. Dado que la refacturación automatizada de Xcode no reemplazó todas las instancias de ese código con una llamada al nuevo método de ayuda, tenemos que hacer eso nosotros mismos. Entonces ahora las pruebas se ven así:

- (void)testNextReminderIdWithNoCurrentReminderIdInUserDefaultsShouldReturnZero { [self setUpUserDefaultsWithCurrentReminderId:nil]; assertThat([sut nextReminderId], is(equalTo(@0))); } - (void)testNextReminderIdWithNoCurrentReminderIdInUserDefaultsShouldSaveZeroInUserDefaults { // given [self setUpUserDefaultsWithCurrentReminderId:nil]; // when [sut nextReminderId]; // then [verify(mockUserDefaults) setObject:@0 forKey:@"currentReminderId"]; }

¿Ves cómo limpiamos continuamente a medida que avanzamos? ¡Las pruebas se han vuelto más fáciles de leer!

Cuarta prueba: con la clave coincidente, devolver el valor incrementado

Ahora queremos probar que si los valores predeterminados del usuario tienen algún valor, devolvemos uno mayor. Voy a copiar y modificar la prueba "should return zero", usando un valor arbitrario de 3.

- (void)testNextReminderIdWithCurrentReminderIdInUserDefaultsShouldReturnOneGreater { [self setUpUserDefaultsWithCurrentReminderId:@3]; assertThat([sut nextReminderId], is(equalTo(@4))); }

Eso falla, como se desea: "Esperaba <4>, pero era <0>".

Aquí hay un código simple para pasar la prueba:

- (NSNumber *)nextReminderId { NSNumber *reminderId = [_userDefaults objectForKey:@"currentReminderId"]; if (reminderId) reminderId = @([reminderId integerValue] + 1); else reminderId = @0; [_userDefaults setObject:@0 forKey:@"currentReminderId"]; return reminderId; }

Excepto por setObject:@0 , esto empieza a parecerse a tu ejemplo. Aún no veo nada para refactorizar. (En realidad lo hay, pero no me di cuenta hasta más tarde. Sigamos).

Quinta prueba: con la clave correspondiente, almacene el valor incrementado

Ahora podemos establecer una prueba más: dadas las mismas condiciones, debe guardar la nueva ID de recordatorio en los valores predeterminados del usuario. Esto se hace rápidamente copiando la prueba anterior, alterándola y dándole un buen nombre:

- (void)testNextReminderIdWithCurrentReminderIdInUserDefaultsShouldSaveOneGreaterInUserDefaults { // given [self setUpUserDefaultsWithCurrentReminderId:@3]; // when [sut nextReminderId]; // then [verify(mockUserDefaults) setObject:@4 forKey:@"currentReminderId"]; }

Esa prueba falla, con "Se esperaba una invocación de coincidencia, pero se recibió 0". Para hacerlo pasar, por supuesto, simplemente cambiamos el setObject:@0 para setObject:reminderId . Todo pasa. ¡Terminamos!

Espera, no hemos terminado. Paso 3: ¿Hay algo para refactorizar? Cuando escribí esto por primera vez, dije: "En realidad no". Pero al mirarlo después de ver el episodio 3 de Clean Code , puedo escuchar al tío Bob diciéndome: "¿Cuán grande debe ser una función? 4 líneas está bien, quizás 5. 6 es ... OK. 10 es demasiado grande". Eso es en 7 líneas. ¿Qué me perdí? Debe estar violando la regla de funciones al hacer más de una cosa.

Una vez más, tío Bob: "La única manera de estar realmente seguro de que una función hace una cosa es extraerla hasta que caiga". Esas primeras 4 líneas trabajan juntas; ellos calculan el valor real. Vamos a seleccionarlos, y Refactorizar ▶ Extraer. Según la regla de alcance del tío Bob del episodio 2, le daremos un nombre largo y descriptivo, ya que su alcance de uso es muy limitado. Esto es lo que nos ofrece la refactorización automática:

- (NSNumber *)determineNextReminderIdFromUserDefaults { NSNumber *reminderId = [_userDefaults objectForKey:@"currentReminderId"]; if (reminderId) reminderId = @([reminderId integerValue] + 1); else reminderId = @0; return reminderId; } - (NSNumber *)nextReminderId { NSNumber *reminderId; reminderId = [self determineNextReminderIdFromUserDefaults]; [_userDefaults setObject:reminderId forKey:@"currentReminderId"]; return reminderId; }

Limpiemos eso para hacerlo más apretado:

- (NSNumber *)determineNextReminderIdFromUserDefaults { NSNumber *reminderId = [_userDefaults objectForKey:@"currentReminderId"]; if (reminderId) return @([reminderId integerValue] + 1); else return @0; } - (NSNumber *)nextReminderId { NSNumber *reminderId = [self determineNextReminderIdFromUserDefaults]; [_userDefaults setObject:reminderId forKey:@"currentReminderId"]; return reminderId; }

Ahora cada método es muy ajustado, y es fácil para cualquiera leer las 3 líneas del método principal para ver qué hace. Pero no me siento cómodo al tener esa clave predeterminada de usuario en dos métodos. Vamos a extraer eso en una constante a la cabeza de Example.m:

static NSString *const currentReminderIdKey = @"currentReminderId";

Usaré esa constante donde quiera que aparezca esa clave en el código de producción. Pero el código de prueba continúa usando los literales. Esto nos protege de alguien que accidentalmente cambia esa tecla constante.

Conclusión

Entonces ahí lo tienes. En cinco pruebas, he llegado a TDD al código que pediste. Espero que te dé una idea más clara de cómo TDD y por qué vale la pena. Siguiendo el vals de 3 pasos

  1. Agregue una prueba que falla
  2. Escriba el código más simple que pase, incluso si parece tonto
  3. Refactor (tanto código de producción como código de prueba)

no solo terminas en el mismo lugar. Terminas con:

  • código bien aislado que admite la inyección de dependencia,
  • código minimalista que solo implementa lo que se ha probado,
  • pruebas para cada caso (con las pruebas mismas verificadas),
  • un código de limpieza chirriante con métodos pequeños y fáciles de leer.

Todos estos beneficios ahorrarán más tiempo que el invertido en TDD, y no solo a largo plazo, sino de manera inmediata.

Para un ejemplo que involucra una aplicación completa, obtenga el libro Desarrollo de iOS basado en pruebas . Aquí está mi reseña del libro .