Escribimos muchas pruebas unitarias, desarrollando la aplicaci贸n
SoundCloud para iOS. Las pruebas unitarias se ven bastante hermosas. Son cortos, (con suerte) legibles, y nos dan la confianza de que el c贸digo que escribimos funciona como se esperaba. Pero las pruebas unitarias, como su nombre lo indica, cubren solo un bloque de c贸digo, la mayor铆a de las veces una funci贸n o clase. Entonces, 驴c贸mo detecta los errores que existen en las interacciones entre clases, errores como
p茅rdidas de memoria ?
Fugas de memoria
A veces es bastante dif铆cil detectar un error de p茅rdida de memoria. Existe la posibilidad de una fuerte referencia al delegado, pero tambi茅n hay errores que son mucho m谩s dif铆ciles de detectar. Por ejemplo, 驴es obvio que el siguiente c贸digo puede contener una p茅rdida de memoria?
final class UseCase { weak var delegate: UseCaseDelegate? private let service: Service init(service: Service) { self.service = service } func run() { service.makeRequest(handleResponse) } private func handleResponse(response: ServiceResponse) {
Dado que el
Servicio ya se est谩 implementando, no hay garant铆as con respecto a su comportamiento. Al pasar la funci贸n
handleResponse a una funci贸n privada, que se captura a
s铆 misma , proporcionamos al
Servicio una fuerte referencia a
UseCase . Si el
Servicio decide mantener este enlace, y no tenemos garant铆a de que esto no suceda, se produce una p茅rdida de memoria. Pero con un estudio superficial del c贸digo, no es obvio que esto realmente pueda suceder.
Tambi茅n hay una
publicaci贸n maravillosa
de John Sandell sobre el uso de pruebas unitarias para detectar p茅rdidas de memoria para las clases. Pero con el ejemplo anterior, donde es muy f谩cil omitir una p茅rdida de memoria, no siempre est谩 claro c贸mo escribir una prueba unitaria de este tipo. (Por supuesto, no estamos hablando aqu铆 en t茅rminos de experiencia).
Como escribi贸 Guilherme en una
publicaci贸n reciente , las nuevas caracter铆sticas de la aplicaci贸n SoundCloud para iOS est谩n escritas de acuerdo con "patrones arquitect贸nicos limpios"; con frecuencia, este es un tipo de
VIPER . La mayor铆a de estos m贸dulos
VIPER se crean utilizando lo que llamamos
ModuleFactory . Tal
ModuleFactory toma algunas entradas, dependencias y configuraci贸n, y crea un
UIViewController que ya est谩 conectado al resto del m贸dulo y se puede
insertar en la pila de navegaci贸n.
Este m贸dulo
VIPER puede tener varios delegados, observadores y fallas fuera de control, cada una de las cuales puede hacer que el controlador permanezca en la memoria despu茅s de que se retira de la pila de navegaci贸n. Cuando esto sucede, la cantidad de memoria aumentar谩 y el sistema operativo puede decidir detener la aplicaci贸n.
Entonces, 驴es posible cubrir tantas fugas potenciales escribiendo tan pocas pruebas unitarias como sea posible? Si no, entonces todo esto fue una gran p茅rdida de tiempo.
Pruebas de integraci贸n
La respuesta, como habr谩s adivinado por el t铆tulo de esta publicaci贸n, es s铆. Y lo hacemos a trav茅s de pruebas de integraci贸n. El prop贸sito de la prueba de integraci贸n es probar c贸mo los objetos interact煤an entre s铆. Por supuesto, los m贸dulos
VIPER son grupos de objetos, las p茅rdidas de memoria son una forma de interacci贸n que definitivamente queremos evitar.
Nuestro plan es simple: vamos a utilizar nuestro
ModuleFactory para crear una instancia de un m贸dulo
VIPER . Luego, eliminaremos el enlace al
UIViewController y nos aseguraremos de que todas las partes importantes del m贸dulo se destruyan junto con 茅l.
El primer problema que enfrentamos es que, por naturaleza, no podemos acceder f谩cilmente a ninguna parte del m贸dulo
VIPER que no sea
UIViewController . La 煤nica funci贸n
p煤blica en nuestro
ModuleFactory es
func make () -> UIViewController . Pero, 驴qu茅 pasa si agregamos otro punto de entrada solo para nuestras pruebas? Este nuevo m茅todo se declarar谩 a trav茅s de
interno , por lo que solo podemos acceder a 茅l a trav茅s de la
importaci贸n @testable , el framework
ModuleFactory . Devolver谩 enlaces a todas las partes m谩s importantes del m贸dulo, que luego podr铆amos mantener para que los enlaces d茅biles ingresen en nuestra prueba. En 煤ltima instancia, se ve as铆:
public final class ModuleFactory {
Esto resuelve el problema de la falta de acceso directo a los datos del objeto. Obviamente, esto no es ideal, pero satisface nuestras necesidades, as铆 que pasemos a escribir la prueba. Se ver谩 as铆:
final class ModuleMemoryLeakTests: XCTestCase {
Entonces, tenemos una manera f谩cil de detectar p茅rdidas de memoria en el m贸dulo
VIPER . De ninguna manera es ideal y requiere un cierto trabajo del usuario para cada m贸dulo nuevo que queremos probar, pero esto es ciertamente mucho menos trabajo que escribir pruebas unitarias separadas para cada posible p茅rdida de memoria. Tambi茅n ayuda a identificar p茅rdidas de memoria que ni siquiera sospechamos. De hecho, despu茅s de escribir varias de estas pruebas, se revel贸 que tenemos una prueba que no pasa, y despu茅s de algunas investigaciones, encontramos una p茅rdida de memoria en el m贸dulo. Despu茅s de la correcci贸n, la prueba debe repetirse.
Tambi茅n nos da un punto de partida para escribir un conjunto m谩s general de pruebas de integraci贸n para m贸dulos. Al final, si solo mantenemos un v铆nculo fuerte con el
Presentador y reemplazamos el
UIViewController con
simulacro , podemos falsificar la entrada del usuario, luego invocar los m茅todos del presentador y verificar la visualizaci贸n ficticia de los datos en la
Vista .