Простой, но мощный трюк SwiftUI
Чтобы хорошо начать тестирование SwiftUI, используя ViewInspector
framework, вы можете прочитать это или это.
Одним из элементов, описанных в первом руководстве (и на странице ViewInspector GitHub), является использование @State
в представлении, которое вы хотите протестировать.
В принципе, если ваше мнение выглядит примерно так:
struct ContentView: View { @State var numClicks:Int = 0 var body: some View { VStack{ Button("Click me"){ numClicks += 1 }.id("Button1") Text("\(numClicks)") .id("Text1") .padding() } } }
На самом деле невозможно проверить действие нажатия кнопки на numClicks
.
Приемлемый обходной путь — добавить немного кода в представление, преобразуя его в основном в:
struct ContentView: View { @State var numClicks:Int = 0 internal let inspection = Inspection<Self>() var body: some View { VStack{ Button("Click me"){ numClicks += 1 }.id("Button1") Text("\(numClicks)") .id("Text1") .padding() }.onReceive(inspection.notice) { self.inspection.visit(self, $0) } } }
где Инспекция:
internal final class Inspection<V> { let notice = PassthroughSubject<UInt, Never>() var callbacks: [UInt: (V) -> Void] = [:] func visit(_ view: V, _ line: UInt) { if let callback = callbacks.removeValue(forKey: line) { callback(view) } } } extension Inspection: InspectionEmissary {}
Как вы можете видеть, ContentView
получил новое свойство Inspection, и onReceive его тела получил указание запустить метод посещения свойства Inspection, когда данные публикуются для замеченного издателя.
Это решает проблему, и работающий тестовый пример может выглядеть так:
func testContentView() throws{ let sut = ContentView() _ = sut.inspection.inspect { view in let button = try view.find(viewWithId: “Button1”).button() try button.tap() XCTAssertEqual(try view.actualView().numClicks, 1) let text = try view.find(viewWithId: “Text1”).text() let value = try text.string() XCTAssertEqual(value, “1”) } }
Тем не менее, добавление тестовых «функций» в рабочий код выглядит довольно уродливо. Это может сделать сложное представление еще более сложным, и в нем много избыточного кода, который необходимо скопировать/вставить в каждое представление, которое вы хотите протестировать.
Поэтому я попытался найти что-то более чистое, что добавило бы немного накладных расходов на код тестирования, но оставило бы реальную реализацию представления нетронутой.
Сначала я реализовал простое представление-оболочку, которое могло бы добавить функциональность проверки, необходимую для ViewInspector
:
public let TEST_WRAPPED_ID: String = “wrapped” struct TestWrapperView<Wrapped: View> : View{ internal let inspection = Inspection<Self>() var wrapped: Wrapped init( wrapped: Wrapped ){ self.wrapped = wrapped } var body: some View { wrapped .id(TEST_WRAPPED_ID) .onReceive(inspection.notice) { self.inspection.visit(self, $0) } } } extension TestWrapperView: Inspectable{}
С этим представлением-оболочкой возможно тестирование представления с использованием исходной реализации ContentView
:
func testContentView() throws{ let sut = TestWrapperView(wrapped: ContentView()) _ = sut.inspection.inspect { view in let wrapped = try view.find(viewWithId: TEST_WRAPPED_ID) let button = try wrapped.find(viewWithId: “Button1”).button() try button.tap() let numClicks = try wrapped .view(ContentView.self) .actualView() .numClicks XCTAssertEqual(numClicks, 1) let text = try wrapped.find(viewWithId: “Text1”).text() let value = try text.string() XCTAssertEqual(value, “1”) } }
Надеюсь, вам понравится этот маленький трюк, который немного облегчит тестирование представлений SwiftUI.