En este artículo, Alexey Kondakov, ingeniero de desarrollo con más de 20 años de experiencia en organizaciones como Motorola, Dell y otras grandes empresas digitales internacionales, hablará sobre las características del trabajo con pruebas y las reglas para su eficacia. No se trata solo de que gracias a ellas se reduce el número de errores, sino también del coste de eliminación, que depende de la etapa en la que se detectaron. Según datos de un estudio de IBM: el precio de la eliminación de errores después del lanzamiento es 4-5 veces mayor que la lucha contra ellos en la etapa de diseño. Si esta práctica es tan útil, ¿qué recomendaciones se pueden dar para que las pruebas funcionen realmente durante el desarrollo de aplicaciones para iOS?
<a/>Pruebas unitarias en Swift
¿Por qué vale la pena empezar a hablar de pruebas unitarias sin pasar por otros tipos de pruebas? Las pruebas unitarias, o pruebas modulares, son un tipo de pruebas con las que se pueden comprobar módulos individuales, es decir, partes del código, procesos individuales. Michael Cohn, uno de los teóricos del aseguramiento de la calidad del desarrollo de aplicaciones para Apple, dijo: desde el punto de vista de la eficacia de las pruebas, es importante la cantidad de pruebas de diferentes tipos y la relación de esta cantidad.
Cohn desarrolló una pirámide de pruebas automatizadas en la que el mayor énfasis se pone precisamente en las pruebas modulares. Su cuota en la jerarquía de comprobaciones debe ser nada menos que del 80%. El 20% restante corresponde a las pruebas de integración, aceptación e interfaz. Cohn creía que precisamente las pruebas modulares actúan en el nivel y en el momento en que se puede detectar el máximo de errores con el mínimo esfuerzo. Por lo tanto, la cuestión de asegurar la calidad del desarrollo de iOS es en un 80% una cuestión de pruebas unitarias. Para las pruebas unitarias se pueden utilizar diferentes bibliotecas que se instalan en el entorno corporativo.
Algunas de las más populares son XCTest, UITest. Alexey dio un ejemplo de una prueba-asserta común, cuya esencia consiste en que se ejecutan determinados argumentos a través de una clase o función de código "de combate", y luego se compara el resultado, que resultó de facto, con el esperado, que se colocó en la variable retrievedItem. La comparación de uno y otro ocurre aquí:
XCTAssertEqual(retrievedItem, item)
class checkAuthClass: XCTestCase {
func testPasswordInput() {
var collection = ContentCollection()
let item = Item(id: "an-item", title: "A title")
collection.add(item)
let retrievedItem = collection.item(withID: "an-item")
XCTAssertEqual(retrievedItem, item)
}
}
<a/>1. Limpieza del código y la arquitectura
El concepto de arquitecturas limpias, código limpio se aplica para caracterizar la calidad de la base de código en general. Sin embargo, las pruebas son una de las millones de razones, y posiblemente la más importante, por las que este principio debe seguirse. La presencia de inyecciones de dependencias caóticas (dependency injections), cuando varios elementos del programa no están encapsulados adecuadamente, sino que están entrelazados entre sí y son interdependientes en diferentes niveles, es peligroso. Esto no solo crea situaciones en las que, después del lanzamiento, puede fallar un segmento completamente inesperado de código "de combate". Como afirma Kondakov, tales arquitecturas y tal código son extremadamente difíciles de probar. Si su clase debe recibir cierta información como entrada, entonces debe formarse fuera de la clase/función. Su clase encapsulada independiente no debe formar esta información dentro de sí misma, sino recibirla como entrada. Esto es necesario, entre otras cosas, para que al probar pueda tomar la clase de interés y proporcionarle cualquier información, estando seguro de que en el contexto del proyecto "de combate" funcionará exactamente igual que en la prueba. Por lo tanto, Alexey aconseja mantener las soluciones arquitectónicas elegidas, por ejemplo, si desde el principio se eligió MVP (Model, View, Presenter) como patrón de desarrollo, como suele ser el caso, entonces es necesario mantener las conexiones correctas de estas partes de un proyecto único.
Desafortunadamente, la arquitectura MVC, popular en el pasado, no cumple con este requisito y ahora no se puede recomendar para su uso. Actualmente se presentan muchos ejemplos de arquitecturas, desde MMVM y VIPER generalmente aceptados hasta bastante exóticos, y es tarea de los arquitectos evaluar sus pros y sus contras y elegir la adecuada. Lo principal es que deben permitir separar la responsabilidad de las clases y los métodos, apoyando las inyecciones de código y datos.
Escribir pruebas puede convertirse en un gran problema para los desarrolladores, pero por mi experiencia puedo decir que en la mayoría de los casos los problemas aparecen donde la responsabilidad no está dividida hasta el final. Siguiendo estrictamente los patrones arquitectónicos, escribir pruebas se vuelve una tarea mucho más simple que intentar cubrir con pruebas un código escrito caóticamente. Conclusión: ¿quiere escribir pruebas de forma rápida y con placer? Mantenga estrictamente la limpieza de la arquitectura.
<a/>2. Cubra el código Swift con protocolos
El lenguaje Swift tiene una construcción muy útil "protocolo", que corresponde aproximadamente a las interfaces de otros lenguajes de programación. Como explicó Kondakov, un protocolo es una plantilla que debe cumplir la clase que lo implementa y que establece las direcciones y restricciones para el desarrollo posterior. Esto funciona como poka-yoke (protección contra el tonto). Simplemente no puede crear una clase que cumpla parcialmente con los requisitos del protocolo, el compilador le obligará a escribir una implementación, incluso la más básica. Y al mismo tiempo, los errores se previenen en la etapa en que no es necesario realizar pruebas. Vale la pena utilizar al máximo la funcionalidad de los protocolos para garantizar la posibilidad de probar los componentes de su aplicación de forma independiente. En particular, es importante cubrir con protocolos el nivel en el que la interfaz de usuario de la aplicación en iOS interactúa con el "backend": bases de datos, solicitudes de red. Si todo esto está unificado y hecho correctamente, no será difícil crear objetos de simulación para las pruebas modulares. Además, las pruebas dirigidas a ellos reflejarán cómo realmente funcionarán sus originales "de combate", de los que depende el funcionamiento de la aplicación.
<a/>3. "Mocks" y mejores prácticas
Los "mocks" son clases, funciones y otros componentes que imitan el funcionamiento de sus prototipos reales de "producción" y son capaces de inyectar los datos de prueba que necesita en el momento correcto. Las copias pueden ser un análogo completo del original, o pueden ser una variante simplificada del mismo. Existen cinco tipos de "mocks", pero ahora no nos detendremos en esto. Esta es una historia completamente diferente. Solo diremos que los "mocks" se crean para simular de forma segura las dependencias externas e internas, es decir, pasar al objeto o función ciertos datos y ver cómo los procesa el "mock". Con frecuencia, la parte "frontal" de la aplicación para Apple, escrita en Swift, interactúa a través de solicitudes de red (HTTP) con la API en C o Java, puede simular la recepción por parte de algún objeto Swift de json, como si viniera de la API. Pasarlo al objeto y realizar una prueba unitaria por algún criterio
{
"glossary":
{
"title":
"example glossary",
{
"GlossDiv":
{
"title": "S",
}
}
"GlossList": {
"GlossEntry": {
"ID": "SGML",
"SortAs": "SGML",
"GlossTerm": "Standard Generalized Markup Language",
"Acronym": "SGML",
"Abbrev": "ISO 8879:1986",
"GlossDef": {
"para": "A specification, used to create markup languages such as DocBook.",
"GlossSeeAlso": ["GML", "XML"]
},
"GlossSee": "markup"
}
}
}
<a></a>4. Если руководство требует покрыть тестами user interface (UI)
}
<a></a>5. Тестирование UIViewController
● organice la combinación de clases mock, que crean desarrolladores individuales para probar sus componentes del proyecto, en bibliotecas de clases mock. Esto evitará perder tiempo reinventando la rueda por centésima vez y permitirá utilizar soluciones ya probadas. Ejemplo: imagine que se está escribiendo una solicitud de red para verificar una de las clases. Con gran probabilidad, se puede decir que dicha solicitud será necesaria en decenas de pruebas que se desarrollarán en un futuro próximo. Los desarrolladores primero deben verificar en la biblioteca si ya se ha escrito lo que necesitan y luego perder tiempo escribiendo su propio "mock";
● insista en un alto nivel de abstracción al escribir clases de pruebas unitarias. De lo contrario, al reutilizar un "mock" de su biblioteca, cuando sea necesario pasarle una solicitud de red "pesada" con un JSON enorme, la clase de prueba tendrá que escribir cientos de líneas de configuración, lo que reducirá a cero la eficiencia del intercambio de clases listas entre desarrolladores;
● utilice bibliotecas para la inversión de dependencias en el proyecto. Ya he hablado de la necesidad de una buena arquitectura de proyecto para pruebas eficaces. Para que la creación de "mocks" basados en sus objetos reales sea eficaz, utilice bibliotecas de conexión de dependencias. Por ejemplo, SWinject. Al igual que los protocolos, permitirá garantizar la inversión de dependencias, es decir, transferir fuera de la clase y el método lo que de otro modo se formaría dentro de él, a través del constructor o la llamada de un objeto dentro del método. Es necesario asegurarse de que cualquier clase y método en su versión de proyecto "de batalla" solo procese la información, y no la obtenga/cree dentro. Si esto no se implementa, las pruebas de mocking se complicarán mucho. Tampoco será posible tomar un método por separado e imitar su funcionamiento dentro del proyecto, creando "fakes" y "stubs": una imitación de las verdaderas interconexiones en el proyecto.
● неправильно проверять цвет текста, шрифт и размер в UILabel;
Las conclusiones de Michael Cohn mencionadas anteriormente, según las cuales no se debe prestar mucha atención a las pruebas de interfaz, dejan claro que es mejor centrarse en las pruebas unitarias. Además, las pruebas unitarias para verificar las interfaces se consideran demasiado largas de escribir. Sin embargo, es posible que los clientes hagan solicitudes que obliguen a la dirección a exigir una cobertura completa de las clases de interfaz con pruebas unitarias. En opinión de Alexéi, lo mejor es que se tome la decisión de mejorar las pruebas modulares para que los problemas no se manifiesten en las interfaces de usuario. Pero si ya tuvo que realizar pruebas de interfaz, debe saber que no cualquier framework diseñado para pruebas unitarias es adecuado para esto. No me detendré en las pruebas de SwiftUI, sino que me centraré más en la posibilidad de probar el código basado en UIKit.
Алексей уточняет, что вместо этого надо сосредоточиться на содержимом. Вот правильные вопросы, которые нужно себе задавать, готовя модульные тесты для интерфейса:
En la arquitectura MVP típica para una aplicación iOS en Swift, los llamados controladores (clases intermediarias) gestionan la interacción de las clases que acceden a la base de datos y las clases View que muestran al usuario pantallas y elementos de la interfaz. El principal entre los controladores es UIViewController. Esta clase se hereda del UIViewController estándar y su uso se recomienda en la documentación oficial para desarrolladores de iOS de Apple. Una estrategia típica para verificar las interfaces es la prueba de extremo a extremo y la prueba de capturas de pantalla. Las capturas de pantalla son buenas para probar el componente visual, pero no dicen nada sobre el contenido de la página. Las pruebas de extremo a extremo (XCUI-driven-tests) están demasiado lejos del código que verifican, por lo que pueden pasar por alto muchas cosas. En caso de error, será extremadamente difícil rastrear el problema. Ambos tipos de pruebas son muy costosos desde el punto de vista de la economía del desarrollo, ya que la aplicación UI cambia constantemente. Es posible probar las interfaces a nivel del trabajo de los controladores y otros elementos del UI kit con la ayuda de pruebas unitarias. Sin embargo, las pruebas modulares de las clases de controladores que muestran la View de la aplicación pueden ser peores que inútiles si no está verificando lo correcto. Entonces, ¿qué debemos probar?
Dado que con las pruebas modulares es imposible verificar la apariencia de lo que ve el usuario, debemos probar los estilos y las rutas de UIViewController y UIView. Como ejemplo de cómo se puede hacer esto incorrectamente, daré algunos ejemplos concretos:
● es incorrecto probar el color de fondo de un elemento en UIView;
● es incorrecto verificar el color del texto, la fuente y el tamaño en UILabel;
● es incorrecto verificar Auto layout y restricciones.
<a></a>6. Mockingbird и модульное тестирование Swift
● ¿Se forma la cantidad correcta de celdas de la cuadrícula en la interfaz?
<a></a>7. Mockingbird vs builder class
● ¿Está habilitado el botón?
● ¿Es correcto el frame de UIView?
Los controladores que gestionan la salida se pueden probar si están aislados de sus dependencias. Ya se mencionó DI – dependency injections – anteriormente. Durante las pruebas, las dependencias reales se reemplazan por simulaciones. Para que sea fácil realizar estas pruebas, los controladores deben ser pasivos. Esto significa que solo enrutan al usuario en respuesta a eventos y muestran pantallas. ¡No se deben extraer datos del modelo directamente en el controlador! ¡No se deben actualizar a sí mismos desde el modelo! Esto hará imposible escribir "mocks" completos.
.withSubtitle(article.subtitle)
Kondakov también señala que este artículo no estaría completo si no se mencionaran los frameworks útiles. Por un lado, esto facilita el aislamiento del elemento investigado de las dependencias de la limpieza del código y la arquitectura, por otro lado, las bibliotecas pueden acelerar el trabajo de prueba, muchas soluciones en las que están integradas "bajo el capó" y no requieren "reinventar la rueda". Una biblioteca como Mockingbird en Swift es indispensable porque permite crear "mocks", stubs, заглушки con un par de líneas de código, que simulan las dependencias reales con las que trabaja el objeto investigado.
.build()
A veces sucede que el aspecto necesario del trabajo de un objeto no se puede verificar con la ayuda de bibliotecas. En este caso, puede recurrir a las capacidades del propio lenguaje. En primer lugar, al "builder class". Escriba el experimento necesario y luego verifique el trabajo con la ayuda de la función build(). Alexei atribuye a la ventaja de este método el hecho de que no está limitado por la estrecha funcionalidad del framework y puede hacer con él todo lo que pueda en el lenguaje Swift. Aquí hay un ejemplo de un "builder":
<a></a>Unit-тестирование в Objective C
.withTitle(article.title)
<a></a>1. OCMock и swizzling: встроенные возможности языка
.withImage(article.image)
<a></a>2. OCMock как рабочий вариант
El "builder" para pruebas unitarias también se puede utilizar para verificar el lenguaje del "backend" de muchas aplicaciones de iOS. Estamos hablando de Objective C.
● если OCMock ограничен в вашем окружении (блокируется чем-либо) или Вам нужно “переопределить” init-метод, корректная работа с которым OCMock не гарантируется - используйте swizzing. Это более громоздкий метод, но иногда только он позволяет достигнуть нужного результата;
La parte de bajo nivel de las aplicaciones para Apple, que trabaja con datos, se escribe en Objective C con más frecuencia de lo que cabría esperar. En particular, este lenguaje se utiliza para este propósito en muchas corporaciones, en las que todavía una parte notable del código heredado está escrita en él. ¿Cómo someter a pruebas unitarias la parte del proyecto que está escrita en este lenguaje? Vamos a averiguarlo.
<a></a>1. OCMock y swizzling: posibilidades integradas del lenguaje
El uso del objeto OCMock y el método swizzling para crear "mocks" a partir de la biblioteca integrada en Objective C requiere más cantidad de código para escribir la prueba, pero ofrece muchas más opciones en la solución de problemas de un texto específico que otras bibliotecas. Por ejemplo, si la clase es entregada por una biblioteca externa, entonces simular sus dependencias sin OCMock se vuelve extremadamente difícil. Con el método swizzling, lo imposible se vuelve posible.
<a></a>2. OCMock como opción de trabajo
Aquellos que están acostumbrados a Swift, pueden no mirar hacia el objeto OCMock al probar objetos en Objective C, mientras tanto, es precisamente en este lenguaje donde se convierte en una alternativa real a la funcionalidad de otras bibliotecas, ya que es precisamente en Objective C donde el método swizzling no está limitado de ninguna manera y se puede utilizar al máximo. Alexei Kondakov dio varias recomendaciones al probar en Objective C utilizando el objeto OCMock: