Bienvenido, soy Luis y esta vez les traigo un nuevo post.
Índice
Mejore sus pruebas unitarias y aumente la capacidad de mantenimiento
Una de las cosas más difíciles de probar su código Swift es comenzar. El lienzo en blanco de una aplicación nueva puede ser intimidante y es fácil posponer la escritura de sus pruebas hasta que haya terminado de escribir código.
Esto a menudo conduce a una situación en la que ha escrito mucho código y, de repente, se vuelve aún más difícil comenzar a escribir pruebas porque, bueno, su código esencialmente no se puede probar.
Esto no se debe a que sea un mal codificador; el código simplemente no se escribió teniendo en cuenta la capacidad de prueba.
En este artículo, veremos cómo la inyección de dependencia puede ayudarlo a diseñar sus aplicaciones por adelantado y resolver algunos de sus problemas de prueba. Incluso te ayudará a refactorizar más adelante.
Introducción a la inyección de dependencia
La idea principal que se debe extraer de la inyección de dependencia (DI)
es que es una técnica en la que un determinado objeto (por ejemplo, un controlador de vista) puede recibir un objeto del que depende (por ejemplo, un modelo de vista) a través de algún tipo de método de configuración o propiedad .
Lo bueno de esto es que no está vinculado a ninguna arquitectura de software específica (MVC
o MVVM
) y no requiere ningún marco de terceros para aplicarlo (aunque tales marcos existen). En realidad, es un patrón de diseño bastante básico, pero le brinda una gran flexibilidad.
Normalmente, hay tres formas de inyectar una dependencia:
- A través de un inicializador personalizado: una gran opción si tiene el control total de la inicialización del objeto (clases personalizadas, estructuras, etc.), pero no es una opción si no lo tiene (por ejemplo, controladores de vista).
- A través de una propiedad: simplemente cree una propiedad de acceso público que le permita establecer la dependencia. Agradable y simple, pero no si no desea que la dependencia sea accesible para otros.
- A través de un método de configuración: le permite establecer la dependencia fácilmente y también le permite ocultar la dependencia del mundo exterior. Genial si desea asegurarse de que otras piezas de código no accedan a sus dependencias directamente.
Cada uno es más o menos igual, pero ofrece (des) ventajas ligeramente diferentes. Exploraremos DI
usando un código de muestra que inyecta un servicio web en un modelo de vista.
Supongamos que estamos creando un modelo de vista que necesita buscar una lista de autores de libros para mostrar en nuestra aplicación. El código podría verse así:
struct Author { var firstName: String var lastName: String var booksWritten: Int } class AuthorViewModel { private let service = AuthorWebService() func allAuthors() -> [Author] { // fetch authors through the service and return them service.fetchAllAuthors() } }
Aunque hemos hecho una buena separación entre nuestro modelo de vista y el servicio real que busca a los autores, hay un problema aquí: no podemos probar nuestro modelo de vista de forma aislada porque llama al servicio y no hay forma de evitar que eso suceda.
Idealmente, nos gustaría probar el modelo de vista y el servicio por separado. Si tuviéramos que probar el modelo de vista tal como está, estaríamos probando demasiado y tendríamos dificultades en el futuro si quisiéramos refactorizar o reemplazar nuestro servicio.
Resolveremos este problema agregando la opción de inyectar el servicio y cambiarlo para que pueda cambiarse fácilmente a un servicio diferente (con fines de prueba).
Dado que tenemos control total sobre esta clase personalizada, elegiremos el enfoque basado en inicializador. El resultado se ve así:
protocol AuthorServiceProtocol { func fetchAllAuthors() -> [Author] } class AuthorWebService: AuthorServiceProtocol { func fetchAllAuthors() -> [Author] { // fetch and return authors } } class AuthorViewModel { private let service : AuthorServiceProtocol init(service: AuthorServiceProtocol = AuthorWebService()) { self.service = service } func allAuthors() -> [Author] { service.fetchAllAuthors() } }
Las conclusiones clave aquí son:
- Hemos creado un protocolo que nuestro servicio debe cumplir. Esto asegura que tengamos un contrato adecuado para el aspecto que debería tener un servicio. Nos hemos asegurado de que nuestro servicio web (existente) implemente este protocolo.
- Hemos cambiado nuestro modelo de vista para aceptar un objeto conforme a este protocolo a través de un inicializador personalizado. Esto se establecerá en la propiedad
service
. - Ofrecemos un servicio predeterminado mediante el uso de un parámetro predeterminado. Esto nos proporciona un sano valor predeterminado cuando el objeto se inicializa sin un objeto de servicio personalizado.
Con esto en su lugar, podemos pasar a probar nuestro código.
Prueba del modelo de vista
Ahora que hemos separado completamente el modelo de vista y el servicio, podemos probar el modelo de vista por sí mismo.
Para hacer esto, construimos un objeto de servicio falso (conforme al protocolo que acabamos de crear) y lo inyectamos durante la prueba:
class FakeAuthorService: AuthorServiceProtocol { func fetchAllAuthors() -> [Author] { [ Author(firstName: "Frank", lastName: "Herbert", booksWritten: 27), Author(firstName: "Roald", lastName: "Dahl", booksWritten: 19), Author(firstName: "Haruki", lastName: "Marukami", booksWritten: 14) ] } } class AuthorViewModelTests: XCTestCase { func testFetchAllAuthors() throws { let viewModel = AuthorViewModel(service: FakeAuthorService()) let authors = viewModel.allAuthors() XCTAssertEqual(authors.count, 3) XCTAssertEqual(authors.first!.firstName, "Frank") } }
Por supuesto, este es un ejemplo muy trivial, pero la conclusión clave sigue siendo: al proporcionar una forma de configurar las dependencias de su objeto y no restringirlas a las codificadas, hace que su código sea mucho más fácil de probar y mantener.
Por ejemplo, si en algún momento prefiere obtener una lista del disco Authors
o de Core Data, simplemente puede escribir un nuevo servicio, ajustarlo AuthorServiceProtocol
e intercambiarlo en su lugar.
El modelo de vista no necesitaría saber nada sobre esto y todas las pruebas para el modelo de vista seguirían funcionando como si nada cambiara.
En resumen
La inyección de dependencia puede ser un concepto complicado de comprender, no porque sea difícil en sí mismo, sino porque los marcos pretenden facilitarlo.
Intentan proporcionar una forma muy rígida, centralizada y estructurada de hacer esto, lo que a menudo conduce a una repetición confusa y una sensación abrumadora de "luchar contra el sistema"
.
Este artículo muestra una forma muy fácil (y común) de comenzar con DI
y demuestra el beneficio de usarlo, si no para propósitos de prueba, entonces para mantenimiento.
DI
no lo obliga a usar una arquitectura determinada para su aplicación y no requiere que use MVC
, MVVM
, Clean o cualquiera de las otras arquitecturas populares.
Es simplemente un patrón de diseño que puede ayudarlo a dividir su código en partes bien definidas, cada una con sus propias responsabilidades.
Gracias por leer este post.
Añadir comentario