La arquitectura equilibrada de la aplicación móvil prolonga la vida del proyecto y los desarrolladores.
En el ultimo episodio
Parte 1 - Componentes básicos de la arquitectura y cómo funciona la arquitectura componible
Código comprobable
En la versión anterior, se desarrolló un marco de aplicación de lista de compras utilizando Arquitectura Composable . Antes de continuar aumentando la funcionalidad, debe guardar: cubra el código con pruebas. En este artículo, consideraremos dos tipos de pruebas: pruebas unitarias para el sistema y pruebas instantáneas para la interfaz de usuario.
¿Que tenemos?
Echemos otro vistazo a la solución actual:
- el estado de la pantalla se describe en la lista de productos;
- dos tipos de eventos: cambiar un producto por índice y agregar uno nuevo;
- el mecanismo que procesa acciones y cambia el estado del sistema es un competidor brillante para escribir pruebas.
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
}
}
)
Tipos de prueba
¿Cómo entender que la arquitectura no es muy buena? Fácil si no puedes cubrirlo al 100% con pruebas (Vladislav Zhukov)
No todos los patrones arquitectónicos definen claramente los enfoques de prueba. Veamos cómo Composable Arhitecutre resuelve este problema.
Pruebas unitarias
Composable Arhitecutre 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
Fuentes de prueba de Snaphsot : github.com