Arsitektur Komposabel adalah pandangan baru tentang arsitektur aplikasi. Tes

Arsitektur yang seimbang dari aplikasi seluler memperpanjang umur proyek dan pengembang.



Di episode terakhir



Bagian 1 - Komponen Arsitektur Dasar dan Cara Kerja Arsitektur Kompos



Kode yang dapat diuji



Pada rilis sebelumnya, framework aplikasi daftar belanja dikembangkan menggunakan Composable Architecture . Sebelum melanjutkan untuk meningkatkan fungsionalitas, Anda perlu menyimpan - menutupi kode dengan tes. Dalam artikel ini, kami akan mempertimbangkan dua jenis pengujian: pengujian unit untuk sistem dan pengujian snapshot untuk UI.



Apa yang kita miliki?



Mari kita lihat lagi solusi saat ini:



  • status layar dijelaskan oleh daftar produk;
  • dua jenis peristiwa: mengubah produk dengan indeks dan menambahkan yang baru;
  • mekanisme yang memproses tindakan dan mengubah status sistem merupakan pesaing yang baik untuk menulis tes.


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


Jenis pengujian



Bagaimana memahami bahwa arsitektur tidak terlalu bagus? Mudah jika Anda tidak dapat menutupinya 100% dengan tes (Vladislav Zhukov)

Tidak semua pola arsitektur dengan jelas mendefinisikan pendekatan pengujian. Mari kita lihat bagaimana Composable Arhitecutre memecahkan masalah ini.



Tes unit



Salah satu alasan untuk menyukai Composable Arhitecutre adalah cara Anda menulis pengujian unit.



alt gambar

โ€” 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
                )
            ]
        }
    )
}


, .



alt gambar



:



gambar



, , .



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


:



gambar



.



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



Sumber pengujian Snaphsot : github.com




All Articles