Depuración y pruebas

Depuración en Xcode

Depuración “clásica” con NSLog y Asserts

  • La depuración clásica consiste en alterar el código fuente de forma temporal añadiendo una serie de directivas con el fin de mostrar mensajes informativos en la consola. Como ya hemos visto, la función habitual para enviar mensajes a la consola de XCode es NSLog()

Antes de generar el binario de distribución para la App Store deberíamos eliminar todas las llamadas a NSLog que tengamos en el código para así dejar "limpia" la aplicación y evitar una posible relentización de la ejecución debido a la escritura en la consola.

  • Otro tipo de función con la cual podemos depurar nuestro código por consola es el llamado Assert. Los Asserts (o aserciones) son condiciones que se deben de cumplir para que continue la ejecución en un determinado momento. Estas deben de devolver true. En el caso de que algún assert no se cumpla se producirá una excepción en fase de ejecución que detendrá la aplicación en ese mismo instante almacenando todo el "log" en XCode para su posterior depuración.

  • La directiva assert en Objective-C viene en forma de macro assert() a la cual se le pasa una condición que será que se tenga que evaluar a verdadero o falso. Si se evalúa a falso la aplicación se detendrá mostrando en la consola un log y si se evalúa a verdadero la aplicación continuará con su ejecución. Los asserts son usados frecuementes en APIs así como en códigos en fases de testeo.

assert(valor < maximoValor);
  • assert hace que el programa aborte inmediatamente cuando falla la aserción

  • De modo alternativo podemos usar NSAssert, que a diferencia de asssert:

    • Cuando falla la aserción no aborta directamente sino que genera una excepción de tipo NSInternalInconsistencyException (que por supuesto también causará un crash si no la capturamos).

    • Nos permite especificar un mensaje de error con argumentos

NSAssert(valor < maximoValor, @"El valor %i es demasiado grande (max.:%i)!", valor, maximoValor);

El depurador integrado de Xcode

El depurador visual integrado en Xcode es muy similar a los que hay en otros IDES. Podemos fijar breakpoints clicando en el margen izquierdo del código, y una vez parada la ejecución podemos ver el valor de las variables, continuar con la ejecución hasta el siguiente breakpoint, ir paso a paso, etc.

A diferencia de otros IDEs no hay un modo especial de ejecución para depurar el código, se parará en los breakpoints cuando ejecutamos la aplicación normalmente con Product > Run (o con el botón de “play” de la barra de herramientas). No obstante, en el “Breakpoint Navigator” podemos deshabilitar todos los breakpoints si queremos ignorarlos en un momento dado.

Una funcionalidad muy interesante es la de Quick Look, que nos permite ver el contenido de ciertas variables de forma “gráfica”. Por ejemplo si se trata de un UIImage podemos ver una versión en pequeño de la imagen para comprobar rápidamente que corresponde con lo que esperamos

  • Como se ve en la figura anterior, cuando estamos depurando el código y la ejecución está parada en un punto, pasando el ratón por encima de una variable aparece un tooltip con información sobre ella. Pulsando sobre el pequeño icono en forma de “ojo” que aparece en el tooltip activamos el quick look. También podemos hacerlo pulsando el mismo icono en el “área de depuración” (parte inferior de la pantalla), donde aparecen las variables actuales

  • Quick look funciona con muchos tipos de variables: vistas (componentes de la interfaz de usuario, que como hemos visto aparecen “en pequeño”), localizaciones geográficas (aparece un mapa centrado en la localización), frames de vistas (aparece un rectángulo con las proporciones adecuadas y una anotación con el tamaño actual),curvas dibujadas con el API de gráficos, …

Testing

  • XCode tiene desde la versión 5 un framework de testing llamado XCTest (en las versiones anteriores se usaba otro distinto, denominado OCUnit).

  • Desde Xcode 5, cuando creamos un proyecto se crea automáticamente un conjunto de pruebas unitarias vacío y un target para ejecutar esas pruebas con el mismo nombre del proyecto pero acabado en "Test". Si tenemos un proyecto ya creado sin pruebas unitarias, podemos crearlas con File > New > Target > Cocoa touch unit test.

  • En XCTest hay varios tipos distintos de pruebas unitarias:

    • Tests de “lógica”: las clásicas pruebas en las que comprobamos si determinado método funciona o no correctamente.

    • Tests de “lógica” en modo asíncrono:

    • Tests de tiempo de respuesta: en los que podemos ver estadísticas del tiempo que tarda en ejecutarse determinado bloque de código. Podemos fijar un baseline de tiempo de modo que el test se considerará que no pasa si está por encima del baseline

El navegador de Tests

Escribir y ejecutar pruebas unitarias

  • Como ya hemos dicho, XCTest es muy similar a otros frameworks de pruebas unitarias como JUnit, así que no es muy complicado de usar para alguien que ya haya usado los otros.

  • Por ejemplo, supongamos que tenemos un juego de tres en raya con el siguiente interfaz (simplificado)

typedef enum {
  Casilla_Vacia,Casilla_X,Casilla_O
} Casilla;

@interface TresEnRayaModelo : NSObject
- (id) init;
- (Casilla) getCasillaFila:(int)fila Columna:(int)columna;
- (void) setCasilla:(Casilla)valor Fila:(int)fila Columna:(int)columna;
@end

Vamos a comprobar varias cosas

  • Que cuando se inicializa el tablero todas las casillas están vacías

  • Que las casillas se pueden obtener/fijar correctamente a un valor dado

  • Que cuando se intenta obtener/fijar una casilla que no existe se produce una excepción

Estructura de una suite de pruebas

  • Iríamos a la plantilla de clase de pruebas que ha creado Xcode e introduciríamos el siguiente código

@interface TresEnRayaTests : XCTestCase
@end

@implementation TresEnRayaTests {
    TresEnRayaModelo *ter;
}

- (void)setUp {
    [super setUp];
    ter = [[TresEnRayaModelo alloc] init];
}

- (void)tearDown {
    // Put teardown code here. This method is called after the invocation of each test method in the class.
    [super tearDown];
}

- (void)testInitDevuelveTableroVacio {
    for (int fila=0;fila<3;fila++)
        for(int col=0;col<3;col++) {
            XCTAssertEqual(Casilla_Vacia, [ter getCasillaFila:fila Columna:col]);
        }
}

-(void)testCasillaIncorrectaGeneraExcepcion {
    XCTAssertThrows([ter getCasillaFila:10 Columna:10], @"Debería lanzar excepción");
    XCTAssertThrows([ter setCasilla:Casilla_X Fila:10 Columna:10], @"Debería lanzar excepción");
}

@end
  • Hay varios puntos a destacar:

    • Por defecto la plantilla coloca el @interface y la @implementation de la clase de la suite de tests en el mismo archivo .m. Esto es adecuado en la mayoría de los casos, ya que no vamos a referenciar la suite desde otras clases y no necesitamos por tanto un @interface separado

    • Al igual que en JUnit hay dos métodos: setup y teardown que se ejecutan al inicio y al final de cada test, respectivamente.

    • Las pruebas unitarias se implementan en métodos que deben devolver void y cuyo nombre debe comenzar por test.

    • Comprobamos el correcto funcionamiento de la lógica con los métodos XCTAssert

Aserciones

En todas las aserciones podemos poner como parámetro final un mensaje (Un NSString) que aparecerá si falla el test

XCTAssertTrue(NO, @"Esta prueba va a fallar seguro");

Tenemos varios tipos de aserciones, por ejemplo:

  • XCTAssertTrue y XCTAssertFalse comprueban que algo es cierto o falso, respectivamente

  • XCTAssertEqual sirve para comprobar la igualdad de valores escalares. En el ejemplo lo hemos usado para comprobar el contenido de las casillas al ser este un enum. Tenemos también el contrario, XCTAssertNotEqual

  • XCTAssertEqualObjects es como el anterior, pero para comparar igualdad entre objetos. Internamente llama al isEqual.

  • XCTAssertThrowscomprueba que una llamada a un método genera una excepción. Opcionalmente, podemos comprobar que además la excepción es de una clase específica.

    Se recomienda consultar la documentación de Apple para más detalles sobre los distintos tipos de aserciones.

Ejecutar las pruebas y ver el resultado

  • En el Test navigator podemos pulsar el pequeño botón de “play” que aparece al pasar el ratón por encima de cada test, para ejecutarlo individualmente o bien el que aparece en la suite completa para ejecutar todas sus pruebas.

  • Se pondrá en marcha el simulador de iOS con la aplicación, ejecutará las pruebas y terminará. En el test navigator y en el código fuente de la suite aparecerá un icono al lado de cada test indicando si ha pasado o no. Si no ha pasado aparecerá además un mensaje en el fuente indicándolo. También podemos ver los mensajes de error en el Log Navigator.

Pruebas de tiempo de respuesta

Con Xcode 6 se introduce un nuevo tipo de tests: los de tiempo de respuesta. Podemos ver estadísticas sobre el tiempo que tarda en ejecutarse un determinado bloque de código. Esto lo hacemos con el método measureBlock, que acepta el bloque a ejecutar como parámetro

- (void) testInitTableroPerformance {
    [self measureBlock:^{
        for(int i=1;i<=100000;i++) {
            TresEnRayaModelo *modelo = [[TresEnRayaModelo alloc] init];
        }
    }];
}

Referencias

Last updated