Introducción

En este apartado vamos estudiar las capacidades de Xcode para generar pruebas que nos permitan detectar errores y evaluar el rendimiento. Estas pruebas se pueden realizar a nivel de código relacionado con los datos o procesos y a nivel de código relacionado con la interfaz gráfica.

Nos centraremos en los mecanismos de pruebas nativos de Xcode relacionados con la clase XCTestCase. Xcode también permite añadir otros frameworks para realizar pruebas como el popular GHUnit; sin embargo, los métodos nativos que explicaremos en este apartado están mucho más integrados con el entorno de desarrollo.

Os dejo el siguiente repositorio https://github.com/al118345/example_ios_test/ con una implementación de ejemplo y el siguiente video

 

4.1 Preparando el proyecto para hacer pruebas

Cuando creamos el proyecto seleccionamos las opciones Include Unit Test e Include UI Test.

start_proyecto.png

Include Unit Test: Nos permitirá la realización de pruebas del código relacionado con datos y procesos.
Include UI Test: Nos permitirá realizar pruebas relacionadas con la interfaz.

En el Project Navigator se nos generarán las carpetas <nombre_proyecto>Tests y <nombre_proyecto>UITest. En este caso, como el proyecto se llama TestSample se generan las carpetas TestSampleTests y TestSampleUITest.

project_navigator_inicio.png

Ahora podemos activar el navegador de pruebas, el rombo con un guion en medio, y veremos los dos módulos de pruebas (targets) creados inicialmente.

test_navigator.png

Cada módulo de pruebas se compone de un conjunto de clases derivadas de XCTestCase y cada clase de un conjunto de métodos. Podemos ejecutar un solo método, todos los métodos de una clase o todos los métodos de todas las clases de un módulo o target. El botón play situado a la derecha del navegador de pruebas indica las pruebas que queremos realizar.

4.2 Unit Test: Pruebas de validación

Dentro de la clase que ha creado Xcode automáticamente rellenamos el método testExample que también nos ha creado. Nosotros podemos crear cualquier otro método con el nombre que queramos. En ocasiones cuando añadimos nuevos métodos de pruebas es necesario hacer un build para que aparezcan en el navegador de pruebas.

Antes de la ejecución de cualquier método de pruebas de esta clase se ejecuta el método setUp. Este método inicializa las variables miembro que podemos necesitar en cualquier método de prueba de la clase. En nuestro ejemplo definimos las propiedades app, appDelegate y viewController y en el método setUp las cargamos.

Swift:

import XCTest
@testable import test_swift
class test_swiftTests: XCTestCase {
var controller: ViewController = ViewController()
override func setUp() {
super.setUp()
}
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
}
func testExample() {
let str_number:String = "12344";
let bResul:Bool = (controller.esDecimal(posible_decimal:str_number))
XCTAssertTrue(bResul, "testExample error. \(str_number)");
}
func testPerformanceExample() {
// This is an example of a performance test case.
self.measure {
// Put the code you want to measure the time of here.
}
}
}

Ya podemos implementar nuestro primer método de pruebas, testExample. Lo que vamos a probar es el método ParseNumber de nuestro ViewController. Este método recibe un NSString y devuelve cierto si representa un número decimal. Para probarlo, le pasamos una cadena que representa un número y nos debe devolver cierto. Si todo es correcto el método XCTAssertTrue se ejecutará de forma correcta y actualizará todos los indicadores dentro de Xcode en consecuencia.

Swift:

func testExample() {
let str_number:String = "12344";
let bResul:Bool = (controller.esDecimal(posible_decimal:str_number))
XCTAssertTrue(bResul, "testExample error. \(str_number)");
}

Al ejecutar el método pulsando el play observamos como aparecen unos indicadores de color verde junto al método, tanto en el código como en el navegador de pruebas. 

indicadores_test_ok1.png

A continuación podemos modificar nuestro método ParseNumber para que devuelva siempre false y veremos cómo reacciona nuestro método de prueba. 

Swift: 

import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
func esDecimal(posible_decimal: String) -> Bool {
let num:Int? = Int(posible_decimal)
return num != nil;
}
}

Volvemos a ejecutar pulsando en el indicador verde junto al método en el navegador de pruebas. Veremos que al pasar con el ratón por encima aparece el botón play.

En este caso aparece un indicador de error en color rojo tanto en el código como en el navegador de pruebas.

indicadores_test_error.png

En el navegador de mensajes y errores también aparece el error con la cadena que hemos configurado en XCTAssertTrue.

navegador_errores_alertas.png

Existe una gran variedad de aserciones del tipo XCTAssertTrue para realizar todas las comprobaciones que consideremos necesarias. Las podéis encontrar en el siguiente link:

https://developer.apple.com/library/ios/documentation/DeveloperTools/Conceptual/testing_with_xcode/chapters/04-writing_tests.html#//apple_ref/doc/uid/TP40014132-CH4-SW34

4.3 Unit Test: Pruebas de rendimiento

 

El objetivo de estas pruebas es determinar el tiempo medio de ejecución de un determinado fragmento de código. El código que queremos estudiar lo colocamos dentro de measureBlock. En este caso la llamada a Parse del objeto viewController.

Swift: 

func testPerformanceExample() {
// This is an example of a performance test case.
self.measure {
controller.Parse()
// Put the code you want to measure the time of here.
}
}

El método Parse de viewController es un método de simulación que simplemente realiza una pausa de dos segundos.

Swift:

func Parse()
{
Thread.sleep(forTimeInterval: 2)
}

Además de los indicadores verdes en la ventana de salida obtenemos el siguiente mensaje:

Test Case '-[TestSampleTests testPerformanceExample]' measured [Time, seconds] average: 2.003, relative standard deviation: 0.094%, values: [2.000132, 2.000050, 2.004982, 2.004994, 2.001563, 2.003315, 2.004982, 2.002713, 2.005006, 2.002375],
performanceMetricID:com.apple.XCTPerformanceMetric_WallClockTime, baselineName: "", baselineAverage: , maxPercentRegression: 10.000%, maxPercentRelativeStandardDeviation: 10.000%, maxRegression: 0.100, maxStandardDeviation: 0.100

Nos indica que la media (average) de tiempo de ejecución de nuestro método es 2.003. Realiza 10 ejecuciones y nos indica el tiempo de cada ejecución: values: [2.000132, 2.000050, 2.004982, 2.004994, 2.001563, 2.003315, 2.004982, 2.002713, 2.005006, 2.002375]
También calcula varios parámetros estadísticos como la desviación tipo, relative standard deviation: 0.094%

Podemos observar cómo las mediciones que realiza la prueba se corresponden a los dos segundos de pausa que hemos indicado en la llamada al método sleepForTimeInterval

Los test de rendimiento no descuentan el tiempo que se detienen en los puntos de interrupción (breakpoint). Es necesario desactivar los puntos de interrupción antes de la ejecución de los test de rendimiento.

4.4 UI Test: Pruebas de interfaz

El objetivo de estas pruebas es evaluar el comportamiento de la aplicación directamente desde los elementos de interfaz que usará el usuario. Se puede considerar una simulación completa de la interacción del usuario.

Las pruebas de interfaz se organizan de la misma forma que las pruebas anteriores: módulos (targets), clases y métodos.

La generación de un método de pruebas tiene dos fases:

  1. Grabación de la interacción del usuario.
  2. Añadir la validación que queremos realizar sobre el resultado.

1. Grabación de la interacción

Pulsamos en un método en el navegador de pruebas y colocamos el cursor dentro del mismo. Vemos que en la parte inferior aparece un botón rojo circular (REC). Este botón ejecutará la aplicación grabando las interacciones del usuario. 

iniciar_grabacion.png

En nuestro ejemplo nos colocamos en el interior del método testExample que se encuentra vacío.

- (void)testExample {

}

Mientras vamos realizando acciones sobre la interfaz se va rellenando el código de nuestro método de prueba. La grabación consiste en la generación de código en el interior del método donde habíamos colocado el cursor. 

En este caso:

  • - Hemos pulsado en un campo de texto de la interfaz.
  • - Hemos escrito “000”.
  • - Hemos pulsado en el botón “HasNumber0

El método queda así:
Swift:

port XCTest
class test_swiftUITests: XCTestCase {
override func setUp() {
// Put setup code here. This method is called before the invocation of each test method in the class.
// In UI tests it is usually best to stop immediately when a failure occurs.
continueAfterFailure = false
// In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
}
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
}
func testExample() {
// UI tests must launch the application that they test.
let app = XCUIApplication()
app.launch()
app.textFields["text"].tap()
app.textFields["text"].typeText("123")
app.buttons["button"].tap()
let value:String = app.textFields["text"].value as! String
XCTAssertEqual(value,"Yes")
}
func testLaunchPerformance() {
if #available(macOS 10.15, iOS 13.0, tvOS 13.0, *) {
// This measures how long it takes to launch your application.
measure(metrics: [XCTOSSignpostMetric.applicationLaunch]) {
XCUIApplication().launch()
}
}
}
}

2. Añadimos la validación

Al final del código que se ha generado automáticamente comprobamos que el texto que aparece en el campo de texto después de pulsar el botón HasNumber0 es “YES”, dado que el código asociado a ese botón comprueba que el texto introducido con anterioridad sea 0.

Y el código del método quedaría en el controller:

override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
@IBOutlet weak var text_field: UITextField!
@IBOutlet var button: UIButton!
@IBAction func esDecimal_presion(_ sender: Any) {
print(esDecimal(posible_decimal: text_field.text ?? "0"))
if esDecimal(posible_decimal: text_field.text ?? "0")
{
text_field.text = "Yes"
}else{
text_field.text = "No"
}
}
func esDecimal(posible_decimal: String) -> Bool {
let num:Int? = Int(posible_decimal)
return num != nil;
}
func Parse()
{
Thread.sleep(forTimeInterval: 2)
}

Seguidamente probamos el método pulsando el play en la parte derecha y se genera la simulación. Cuando finaliza, el resultado de la misma queda señalizado en nuestra vista de pruebas con el indicador verde correspondiente.
 

pasamos_validacion_ui.png