L'architecture équilibrée de l'application mobile prolonge la vie du projet et des développeurs.
Dans le dernier Ă©pisode
Code testable
Dans la version précédente, un cadre d'application de liste d'achats a été développé à l'aide de l' architecture composable . Avant de continuer à développer la fonctionnalité, vous devez enregistrer - couvrir le code avec des tests. Dans cet article, nous examinerons deux types de tests: les tests unitaires pour le système et les tests instantanés pour l'interface utilisateur.
Ce que nous avons?
Jetons un autre regard sur la solution actuelle:
- l'état de l'écran est décrit par la liste des produits;
- deux types d'événements: changer un produit par index et en ajouter un nouveau;
- le mécanisme qui traite les actions et modifie l'état du système est un candidat brillant pour l'écriture de tests.
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
}
}
)
Types de tests
Comment comprendre que l'architecture n'est pas très bonne? Facile si vous ne pouvez pas le couvrir à 100% avec des tests (Vladislav Zhukov)
Tous les modèles architecturaux ne définissent pas clairement les approches de test. Voyons comment Composable Arhitecutre résout ce problème.
Tests unitaires
L'une des raisons d'aimer Composable Arhitecutre est la façon dont vous écrivez des tests unitaires.
— 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
Test des sources Snaphsot: github.com