تعد البنية القابلة للتكوين طريقة جديدة في هندسة التطبيقات. الاختبارات

تطيل البنية المتوازنة لتطبيقات الهاتف المحمول من عمر المشروع والمطورين.



في الحلقة الماضية



الجزء 1 - مكونات العمارة الأساسية وكيف تعمل العمارة القابلة للتركيب



كود قابل للاختبار



في إصدار سابق ، تم تطوير إطار عمل تطبيق قائمة التسوق باستخدام Composable Architecture . قبل الاستمرار في زيادة الوظائف ، تحتاج إلى الحفظ - قم بتغطية الرمز بالاختبارات. في هذه المقالة ، سننظر في نوعين من الاختبارات: اختبارات الوحدة للنظام واختبارات لقطة لواجهة المستخدم.



ما لدينا؟



دعنا نلقي نظرة أخرى على الحل الحالي:



  • حالة الشاشة موصوفة بقائمة المنتجات ؛
  • نوعان من الأحداث: تغيير المنتج حسب الفهرس وإضافة منتج جديد ؛
  • الآلية التي تعالج الإجراءات وتغير حالة النظام هي منافس لامع لكتابة الاختبارات.


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٪ بالاختبارات (فلاديسلاف جوكوف)

ليست كل الأنماط المعمارية تحدد بوضوح نهج الاختبار. دعونا نرى كيف يحل Composable Arhitecutre هذه المشكلة.



اختبارات الوحدة



أحد أسباب حب آلة الكتابة المركبة هي الطريقة التي تكتب بها اختبارات الوحدة.



صورة بديلة

— 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