تطيل البنية المتوازنة لتطبيقات الهاتف المحمول من عمر المشروع والمطورين.
في الحلقة الماضية
الجزء 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
Composable Architecture: https://github.com/pointfreeco/swift-composable-architecture
مصادر Snaphsot الاختبار: github.com