Composable Architecture es una nueva versión de la arquitectura de aplicaciones. Pruebas

La arquitectura equilibrada de la aplicación móvil prolonga la vida del proyecto y los desarrolladores.



En el ultimo episodio



Parte 1 - Componentes básicos de la arquitectura y cómo funciona la arquitectura componible



Código comprobable



En la versión anterior, se desarrolló un marco de aplicación de lista de compras utilizando Arquitectura Composable . Antes de continuar aumentando la funcionalidad, debe guardar: cubra el código con pruebas. En este artículo, consideraremos dos tipos de pruebas: pruebas unitarias para el sistema y pruebas instantáneas para la interfaz de usuario.



¿Que tenemos?



Echemos otro vistazo a la solución actual:



  • el estado de la pantalla se describe en la lista de productos;
  • dos tipos de eventos: cambiar un producto por índice y agregar uno nuevo;
  • el mecanismo que procesa acciones y cambia el estado del sistema es un competidor brillante para escribir pruebas.


struct ShoppingListState: Equatable {
    var products: [Product] = []
}

enum ShoppingListAction {
    case productAction(Int, ProductAction)
    case addProduct
}

let shoppingListReducer: Reducer<ShoppingListState, ShoppingListAction, ShoppingListEnviroment> = .combine(
    productReducer.forEach(
        state: \.products,
        action: /ShoppingListAction.productAction,
        environment: { _ in ProductEnviroment() }
    ),
    Reducer { state, action, env in
        switch action {
        case .addProduct:
            state.products.insert(
                Product(id: UUID(), name: "", isInBox: false),
                at: 0
            )
            return .none
        case .productAction:
            return .none
        }
    }
)


Tipos de prueba



¿Cómo entender que la arquitectura no es muy buena? Fácil si no puedes cubrirlo al 100% con pruebas (Vladislav Zhukov)

No todos los patrones arquitectónicos definen claramente los enfoques de prueba. Veamos cómo Composable Arhitecutre resuelve este problema.



Pruebas unitarias



Composable Arhitecutre unit .



imagen alt

— recuder' — : send(Action) receive(Action). , .



Send(Action) .



Receive(Action) , — action.



.do {} .



.



func testAddProduct() {
    //   
    let store = TestStore(
        initialState: ShoppingListState(
            products: []
        ),
        reducer: shoppingListReducer,
        environment: ShoppingListEnviroment()
    )
    //    
    store.assert(
        //    
        .send(.addProduct) { state in
            //    
            state.products = [
                Product(
                    id: UUID(),
                    name: "",
                    isInBox: false
                )
            ]
        }
    )
}


, .



imagen alt



:



imagen



, , .



Reducer —



?



«» — , .



, UUID . , "".



UUID . Composable Architecture (Environment).



ShoppingListEnviroment () UUID.



struct ShoppingListEnviroment {
    var uuidGenerator: () -> UUID
}


:



Reducer { state, action, env in
    switch action {
    case .addProduct:
        state.products.insert(
            Product(
                id: env.uuidGenerator(),
                name: "",
                isInBox: false
            ),
            at: 0
        )
        return .none
    ...
    }
}


, . :



func testAddProduct() {
    let store = TestStore(
        initialState: ShoppingListState(),
        reducer: shoppingListReducer,
        //  
        environment: ShoppingListEnviroment(
            //     UUID
            uuidGenerator: { UUID(uuidString: "00000000-0000-0000-0000-000000000000")! }
        )
    )
    store.assert(
        //     " "
        .send(.addProduct) { newState in
            //     
            newState.products = [
                Product(
                    //      UUID
                    id: UUID(uuidString: "00000000-0000-0000-0000-000000000000")!,
                    name: "",
                    isInBox: false
                )
            ]
        }
    )
}


, . : saveProducts loadProducts:



struct ShoppingListEnviroment {
    var uuidGenerator: () -> UUID
    var save: ([Product]) -> Effect<Never, Never>
    var load: () -> Effect<[Product], Never>
}


, , Effect. Effect — Publisher. .



:



func testAddProduct() {
    // ,   ,  
    var savedProducts: [Product] = []
    // ,      
    var numberOfSaves = 0
    //   
    let store = TestStore(
        initialState: ShoppingListState(products: []),
        reducer: shoppingListReducer,
        environment: ShoppingListEnviroment(
            uuidGenerator: { .mock },
            //     
            //     
            saveProducts: { products in Effect.fireAndForget { savedProducts = products; numberOfSaves += 1 } },
            //   
            //      
            loadProducts: { Effect(value: [Product(id: .mock, name: "Milk", isInBox: false)]) }
        )
    )
    store.assert(
        //    load   view
        .send(.loadProducts),
        //  load    
        //    productsLoaded([Product])
        .receive(.productsLoaded([Product(id: .mock, name: "Milk", isInBox: false)])) {
            $0.products = [
                Product(id: .mock, name: "Milk", isInBox: false)
            ]
        },
        //     
        .send(.addProduct) {
            $0.products = [
                Product(id: .mock, name: "", isInBox: false),
                Product(id: .mock, name: "Milk", isInBox: false)
            ]
        },
        // ,      
        .receive(.saveProducts),
        //      
        .do {
            XCTAssertEqual(savedProducts, [
                Product(id: .mock, name: "", isInBox: false),
                Product(id: .mock, name: "Milk", isInBox: false)
            ])
        },
        //    
        .send(.productAction(0, .updateName("Banana"))) {
            $0.products = [
                Product(id: .mock, name: "Banana", isInBox: false),
                Product(id: .mock, name: "Milk", isInBox: false)
            ]
        },
        //     endEditing textFiled'a 
        .send(.saveProducts),
        //      
        .do {
            XCTAssertEqual(savedProducts, [
                Product(id: .mock, name: "Banana", isInBox: false),
                Product(id: .mock, name: "Milk", isInBox: false)
            ])
        }
    )
    // ,     2 
    XCTAssertEqual(numberOfSaves, 2)
}


:



  • unit ;
  • ;
  • , .


Unit-Snapshot UI



snapshot , Composable Arhitecture SnapshotTesting ( ).



, :



  • ;
  • ;
  • ;
  • .


Composable Architecture data-driven development, snapshot- — UI .



:



import XCTest
import ComposableArchitecture
//     
import SnapshotTesting
@testable import Composable

class ShoppingListSnapshotTests: XCTestCase {

    func testEmptyList() {
        //  view
        let listView = ShoppingListView(
            //  
            store: ShoppingListStore(
                //  
                initialState: ShoppingListState(products: []),
                reducer: Reducer { _, _, _ in .none },
                environment: ShoppingListEnviroment.mock
            )
        )
        assertSnapshot(matching: listView, as: .image)
    }

    func testNewItem() {
        let listView = ShoppingListView(
            //    store   
            //    Store.mock(state:State)
            store: .mock(state: ShoppingListState(
                products: [Product(id: .mock, name: "", isInBox: false)]
            ))
        )
        assertSnapshot(matching: listView, as: .image)
    }

    func testSingleItem() {
        let listView = ShoppingListView(
            store: .mock(state: ShoppingListState(
                products: [Product(id: .mock, name: "Milk", isInBox: false)]
            ))
        )
        assertSnapshot(matching: listView, as: .image)
    }

    func testCompleteItem() {
        let listView = ShoppingListView(
            store: .mock(state: ShoppingListState(
                products: [Product(id: .mock, name: "Milk", isInBox: true)]
            ))
        )
        assertSnapshot(matching: listView, as: .image)
    }
}


:



imagen



.



Debug mode —



debug:



Reducer { state, action, env in
    switch action { ... }
}.debug()
// 
Reducer { state, action, env in
    switch action { ... }
}.debugActions()


debug , :



received action:
  ShoppingListAction.load
  (No state changes)

received action:
  ShoppingListAction.setupProducts(
    [
      Product(
        id: 9F047826-B431-4D20-9B80-CC65D6A1101B,
        name: "",
        isInBox: false
      ),
      Product(
        id: D9834386-75BC-4B9C-B87B-121FFFDB2F93,
        name: "Tesggggg",
        isInBox: false
      ),
      Product(
        id: D4405C13-2BB9-4CD4-A3A2-8289EAC6678C,
        name: "",
        isInBox: false
      ),
    ]
  )ShoppingListState(
    products: [
+     Product(
+       id: 9F047826-B431-4D20-9B80-CC65D6A1101B,
+       name: "",
+       isInBox: false
+     ),
+     Product(
+       id: D9834386-75BC-4B9C-B87B-121FFFDB2F93,
+       name: "Tesggggg",
+       isInBox: false
+     ),
+     Product(
+       id: D4405C13-2BB9-4CD4-A3A2-8289EAC6678C,
+       name: "",
+       isInBox: false
+     ),
    ]
  )


* .





3 — , (in progress)



4 — (in progress)





2: github.com



: pointfree.co



Composable Architecture: https://github.com/pointfreeco/swift-composable-architecture



Fuentes de prueba de Snaphsot : github.com




All Articles