Pruebas unitarias en arquitectura Clean Swift

Hola lector


No es ningún secreto que las pruebas son una parte integral de cualquier desarrollo. En artículos anteriores, analizamos la arquitectura básica de la arquitectura Clean Swift , y ahora es el momento de aprender cómo cubrir su Unidad con pruebas. Tomaremos el proyecto del artículo sobre Trabajadores como base y analizaremos los puntos principales.



Teoría


Gracias a la inyección de dependencia y al protocolo , todos los componentes de la escena en Clean Swift son independientes entre sí y se pueden probar por separado. Como ejemplo, Interactor depende de Presentador y Trabajador , pero estas dependencias son opcionales y están basadas en protocolos. Por lo tanto, Interactor puede realizar su trabajo (aunque inferior) sin Presentador y Trabajador , así como también podemos reemplazarlos con otros objetos firmados bajo sus protocolos.


Como queremos probar cada componente por separado, debemos reemplazar las dependencias con pseudocomponentes . Esto nos ayudará a los espías (espía). Los espías son objetos de prueba que implementan los protocolos que queremos inyectar y rastrean las llamadas a métodos en ellos. En otras palabras, creamos Spy para Presenter y Worker , y luego los inyectamos en Interactor para rastrear llamadas a métodos.



Para ser justos, agregaré que también hay objetos de prueba ( Dobles de prueba ) Dummy , Fake , Stub y Mock . Pero dentro del marco de este artículo, no los afectaremos. Lea más aquí - TestDoubles


Practica


Terminado con las palabras, vamos al grano. Para ahorrar tiempo, consideraremos el código abstracto sin entrar en detalles de la implementación de cada método. Los detalles del código de la aplicación se pueden encontrar aquí: CleanSwiftTests


Para cada componente de la escena, creamos un archivo con pruebas y dobles de prueba para dependencias de componentes ( Dobles de prueba ).


Un ejemplo de tal estructura:



La estructura de cada archivo de prueba (para componentes) se ve igual y se adhiere aproximadamente a la siguiente secuencia de escritura:


  • Declaramos una variable con SUT (el objeto que vamos a probar) y variables con sus principales dependencias
  • Inicializamos el SUT y sus dependencias en setUp () , y luego los borramos en tearDown ()
  • Métodos de prueba

Como discutimos en teoría, cada componente de la escena se puede probar por separado. Podemos inyectar duplicados de prueba ( Spy ) en sus dependencias y, por lo tanto, monitorear el funcionamiento de los métodos de nuestro SUT . Echemos un vistazo más de cerca al proceso de escribir pruebas usando el ejemplo de Interactor de la escena Home .


HomeInteractor depende de dos objetos: Presentador y Trabajador . Ambas variables en la clase tienen un tipo de protocolo. Esto significa que podemos crear duplicados de prueba firmados bajo los protocolos HomePresentationLogic y HomeWorkingLogic , y luego inyectarlos en HomeInteractor .


final class HomeInteractor: HomeBusinessLogic, HomeDataStore { // MARK: - Public Properties var presenter: HomePresentationLogic? lazy var worker: HomeWorkingLogic = HomeWorker() var users: [User] = [] var selectedUser: User? // MARK: - HomeBusinessLogic func fetchUsers(_ request: HomeModels.FetchUsers.Request) { // ... } func selectUser(_ request: HomeModels.SelectUser.Request) { // ... } 

Probaremos dos métodos:


  • fetchUsers (:) . Responsable de obtener una lista de usuarios por API . Se envía una solicitud de API utilizando Worker .
  • selectUser (:) . Responsable de seleccionar el usuario activo ( selectedUser ) de la lista de usuarios cargados ( usuarios ).

Para comenzar a escribir las pruebas de Interactor , necesitamos crear espías que rastreen la invocación de métodos en HomePresentationLogic y HomeWorkingLogic . Para hacer esto, cree la clase HomePresentationLogicSpy en el directorio 'CleanSwiftTestsTests / Stores / Home / TestDoubles / Spies', firme el protocolo HomePresentationLogic e implemente el método de este protocolo.


 final class HomePresentationLogicSpy: HomePresentationLogic { // MARK: - Public Properties private(set) var isCalledPresentFetchedUsers = false // MARK: - Public Methods func presentFetchedUsers(_ response: HomeModels.FetchUsers.Response) { isCalledPresentFetchedUsers = true } } 

Aquí todo es extremadamente transparente. Si se llamó al método presentFetchedUsers ( HomePresentationLogic ), establecemos el valor de la variable isCalledPresentFetchedUsers en true . Por lo tanto, podemos rastrear si este método fue llamado durante la prueba de Interactor .


Usando el mismo principio, cree HomeWorkingLogicSpy . Una diferencia, llamamos finalización , porque parte del código en Interactor se incluirá en el cierre de este método. Los métodos de HomeWorkingLogic se ocupan de las solicitudes de red. Necesitamos evitar solicitudes de red reales durante las pruebas. Para hacer esto, lo reemplazamos con una toma de prueba, que rastrea las llamadas a métodos y devuelve datos de plantilla, pero no realiza ninguna solicitud a la red.


 final class HomeWorkingLogicSpy: HomeWorkingLogic { // MARK: - Public Properties private(set) var isCalledFetchUsers = false let users: [User] = [ User(id: 1, name: "Ivan", username: "ivan91"), User(id: 2, name: "Igor", username: "igor_test") ] // MARK: - Public Methods func fetchUsers(_ completion: @escaping ([User]?) -> Void) { isCalledFetchUsers = true completion(users) } } 

A continuación, creamos la clase HomeInteractorTests , con la que probaremos HomeInteractor .


 final class HomeInteractorTests: XCTestCase { // MARK: - Private Properties private var sut: HomeInteractor! private var worker: HomeWorkingLogicSpy! private var presenter: HomePresentationLogicSpy! // MARK: - Lifecycle override func setUp() { super.setUp() let homeInteractor = HomeInteractor() let homeWorker = HomeWorkingLogicSpy() let homePresenter = HomePresentationLogicSpy() homeInteractor.worker = homeWorker homeInteractor.presenter = homePresenter sut = homeInteractor worker = homeWorker presenter = homePresenter } override func tearDown() { sut = nil worker = nil presenter = nil super.tearDown() } // MARK: - Public Methods func testFetchUsers() { // ... } func testSelectUser() { // ... } } 

Indicamos tres variables principales: sut , trabajador y presentador .


En setUp (), inicialice los objetos necesarios, inyecte dependencias en Interactor y asigne objetos a las variables de clase.
En tearDown (), borramos las variables de clase para la pureza del experimento.


Se llama al método setUp () antes de que comience el método de prueba, por ejemplo testFetchUsers () y tearDown () cuando este método ha completado su trabajo. Por lo tanto, recreamos el objeto de prueba ( sut ) antes de ejecutar cada método de prueba.


Los siguientes son los métodos de prueba en sí. La estructura se divide en 3 bloques lógicos principales: la creación de los objetos necesarios, el lanzamiento del método probado en SUT y la verificación de los resultados. En el siguiente ejemplo, creamos una solicitud (en nuestro caso, no tiene parámetros), ejecutamos el método fetchUsers (:) Interactor'a , y luego verificamos si se han llamado los métodos necesarios en HomeWorkingLogicSpy y HomePresentationLogicSpy . También verificamos si Interactor ha guardado los datos de prueba recibidos de Worker en su DataStore .


 func testFetchUsers() { let request = HomeModels.FetchUsers.Request() sut.fetchUsers(request) XCTAssertTrue(worker.isCalledFetchUsers, "Not started worker.fetchUsers(:)") XCTAssertTrue(presenter.isCalledPresentFetchedUsers, "Not started presenter.presentFetchedUsers(:)") XCTAssertEqual(sut.users.count, worker.users.count) } 

Probaremos la elección del usuario por una estructura similar. Declaramos las variables expectationId y expectationName , mediante las cuales compararemos el resultado de la selección del usuario. La variable usuarios almacena una lista de prueba de usuarios que asignamos a Interactor . Porque los métodos de prueba se llaman independientemente uno del otro, y en tearDown () ponemos a cero los datos, luego la lista de usuarios de Interactor está vacía y necesitamos llenarla con algo. Y luego verificamos si el usuario fue asignado en el DataStore Interactor'a , después de llamar a sut.selectUser (:) , y si el usuario es el correcto.


 func testSelectUser() { let expectationId = 2 let expectationName = "Vasya" let users = [ User(id: 1, name: "Ivan", username: "ivan"), User(id: 2, name: "Vasya", username: "vasya91"), User(id: 3, name: "Maria", username: "maria_love") ] let request = HomeModels.SelectUser.Request(index: 1) sut.users = users sut.selectUser(request) XCTAssertNotNil(sut.selectedUser, "User not selected") XCTAssertEqual(sut.selectedUser?.id, expectationId) XCTAssertEqual(sut.selectedUser?.name, expectationName) } 

Las pruebas de Presenter'a y ViewController'a se realizan con el mismo principio, con mínimas diferencias. Una de las diferencias es que para probar ViewController, deberá crear una UIWindow y obtener el controlador del Storyboard en setUp () , así como crear objetos Spy en tablas y colecciones. Pero estos matices varían según las necesidades.


Para completar, le recomiendo que se familiarice con el proyecto mediante el enlace al final del artículo.


Conclusión


Hemos cubierto los principios básicos de las aplicaciones de prueba en la arquitectura Clean Swift . No tiene diferencias fundamentalmente fuertes de los proyectos de prueba en otras arquitecturas, todos los mismos dobles de prueba, inyección y protocolos. Lo principal es no olvidar que cada ciclo VIP debe tener una (¡y solo una!) Responsabilidad. Esto hará que el código sea más limpio y las pruebas más obvias.


Enlace al proyecto: CleanSwiftTests
Ayuda para escribir un artículo: Bastien


Serie de artículos


  1. Descripción general de la arquitectura Clean Swift
  2. Enrutador y paso de datos en la arquitectura Clean Swift
  3. Trabajadores en arquitectura Clean Swift
  4. Pruebas unitarias en la arquitectura Clean Swift (estás aquí)
  5. Un ejemplo de una arquitectura simple de tienda en línea Clean Swift

Source: https://habr.com/ru/post/483882/


All Articles