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.
โ 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
Sumber pengujian Snaphsot : github.com