可组合体系结构是对应用程序体系结构的全新尝试。测验

移动应用程序的平衡架构延长了项目和开发人员的寿命。



在最后一集中



第1部分-基本架构组件以及可组合架构的工作方式



可测试的代码



在先前的版本中,使用Composable Architecture开发了购物清单应用程序框架在继续构建功能之前,您需要保存-使用测试覆盖代码。在本文中,我们将考虑两种类型的测试:系统的单元测试和UI的快照测试。



我们有什么?



让我们再来看一下当前的解决方案:



  • 屏幕状态由产品列表描述;
  • 两种事件:按索引更改产品并添加新事件;
  • 处理动作并更改系统状态的机制是编写测试的有力竞争者。


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


测试类型



如何理解架构不是很好?如果您无法通过测试100%覆盖它,这很容易(Vladislav Zhukov)

并非所有的架构模式都明确定义了测试方法。让我们看看Composable Arhitecutre如何解决这个问题。



单元测试



Composable Arhitecutre unit .



图片替代

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


, .



图片替代



:



图片



, , .



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


:



图片



.



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



来源Snaphsot测试:github.com




All Articles