L'architecture composable est une nouvelle approche de l'architecture des applications. Des tests

L'architecture équilibrée de l'application mobile prolonge la vie du projet et des développeurs.



Dans le dernier Ă©pisode



Partie 1 - Les principaux composants de l'architecture et comment fonctionne l'architecture composable



Code testable



Dans la version précédente, un cadre d'application de liste d'achats a été développé à l'aide de l' architecture composable . Avant de continuer à développer la fonctionnalité, vous devez enregistrer - couvrir le code avec des tests. Dans cet article, nous examinerons deux types de tests: les tests unitaires pour le système et les tests instantanés pour l'interface utilisateur.



Ce que nous avons?



Jetons un autre regard sur la solution actuelle:



  • l'Ă©tat de l'Ă©cran est dĂ©crit par la liste des produits;
  • deux types d'Ă©vĂ©nements: changer un produit par index et en ajouter un nouveau;
  • le mĂ©canisme qui traite les actions et modifie l'Ă©tat du système est un candidat brillant pour l'Ă©criture de tests.


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
        }
    }
)


Types de tests



Comment comprendre que l'architecture n'est pas très bonne? Facile si vous ne pouvez pas le couvrir à 100% avec des tests (Vladislav Zhukov)

Tous les modèles architecturaux ne définissent pas clairement les approches de test. Voyons comment Composable Arhitecutre résout ce problème.



Tests unitaires



L'une des raisons d'aimer Composable Arhitecutre est la façon dont vous écrivez des tests unitaires.



image 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
                )
            ]
        }
    )
}


, .



image alt



:



image



, , .



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)
    }
}


:



image



.



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



Test des sources Snaphsot: github.com




All Articles