use*_*387 9 swift swiftui combine
我正在尝试在合并框架和 SwiftUI 中测试一个简单的发布者。我的测试在我的视图模型中测试了一个名为 isValid 的已发布布尔值。我的视图模型还有一个已发布的用户名字符串,当该字符串发生更改并变为 3 个或更多字符时,isValid 会被分配该值。这是视图模型。我确信我不了解发布商如何在测试环境、时间安排等中工作......提前致谢。
public class UserViewModel: ObservableObject {
@Published var username = ""
@Published var isValid = false
private var disposables = Set<AnyCancellable>()
init() {
$username
.receive(on: RunLoop.main)
.removeDuplicates()
.map { input in
print("~~~> \(input.count >= 3)")
return input.count >= 3
}
.assign(to: \.isValid, on: self)
.store(in: &disposables)
}
}
Run Code Online (Sandbox Code Playgroud)
这是我的观点,这里并不重要
struct ContentView: View {
@ObservedObject private var userViewModel = UserViewModel()
var body: some View {
TextField("Username", text: $userViewModel.username)
}
}
Run Code Online (Sandbox Code Playgroud)
这是我的测试文件和失败的单个测试
class StackoverFlowQuestionTests: XCTestCase {
var model = UserViewModel()
override func setUp() {
model = UserViewModel()
}
override func tearDown() {
}
func testIsValid() {
model.username = "1"
XCTAssertFalse(model.isValid)
model.username = "1234"
XCTAssertTrue(model.isValid) //<----- THIS FAILS HERE
}
}
Run Code Online (Sandbox Code Playgroud)
原因是视图模型异步但测试是同步的......
$username
.receive(on: RunLoop.main)
Run Code Online (Sandbox Code Playgroud)
...这里的运算符对下一个事件周期.receive进行最终分配isValidRunLoop.main
但测试
model.username = "1234"
XCTAssertTrue(model.isValid) //<----- THIS FAILS HERE
Run Code Online (Sandbox Code Playgroud)
预计isValid会立即改变。
所以有以下可能的解决方案:
完全删除.receive运算符(在这种情况下,这是更好的选择,因为它是 UI 工作流程,无论如何总是在主运行循环上,因此使用计划接收是多余的。
$username
.removeDuplicates()
.map { input in
print("~~~> \(input.count >= 3)")
return input.count >= 3
}
.assign(to: \.isValid, on: self)
.store(in: &disposables)
Run Code Online (Sandbox Code Playgroud)
结果:
model.username = "1234"
XCTAssertTrue(model.isValid) // << PASSED
Run Code Online (Sandbox Code Playgroud)
让 UT 等待一个事件,然后才进行测试isValid(在这种情况下,应记录其isValid有意具有异步性质)
model.username = "1234"
RunLoop.main.run(mode: .default, before: .distantPast) // << wait one event
XCTAssertTrue(model.isValid) // << PASSED
Run Code Online (Sandbox Code Playgroud)
正如@Asperi所说:这个错误的原因是你异步接收值。我查了一下,找到了苹果的使用教程XCTestExpectation。所以我尝试将它与您的代码一起使用并且测试成功通过。另一种方法是使用合并期望。
class StackoverFlowQuestionTests: XCTestCase {
var model = UserViewModel()
override func setUp() {
model = UserViewModel()
}
func testIsValid() throws {
let expectation = self.expectation(description: "waiting validation")
let subscriber = model.$isValid.sink { _ in
guard self.model.username != "" else { return }
expectation.fulfill()
}
model.username = "1234"
wait(for: [expectation], timeout: 1)
XCTAssertTrue(model.isValid)
}
func testIsNotValid() {
let expectation = self.expectation(description: "waiting validation")
let subscriber = model.$isValid.sink { _ in
guard self.model.username != "" else { return }
expectation.fulfill()
}
model.username = "1"
wait(for: [expectation], timeout: 1)
XCTAssertFalse(model.isValid)
}
}
Run Code Online (Sandbox Code Playgroud)
更新
为了清楚起见,我添加了所有代码和输出。我像您的示例一样更改了测试验证(您测试“1”和“1234”选项)。您会看到,我只是复制粘贴您的模型(名称和public变量除外init())。但我仍然没有犯这个错误:
异步等待失败:超时超过 1 秒,未满足期望:“等待验证”。
// MARK: TestableCombineModel.swift file
import Foundation
import Combine
public class TestableModel: ObservableObject {
@Published public var username = ""
@Published public var isValid = false
private var disposables = Set<AnyCancellable>()
public init() {
$username
.receive(on: RunLoop.main) // as you see, I didn't delete it
.removeDuplicates()
.map { input in
print("~~~> \(input.count >= 3)")
return input.count >= 3
}
.assign(to: \.isValid, on: self)
.store(in: &disposables)
}
}
// MARK: stackoverflowanswerTests.swift file:
import XCTest
import stackoverflowanswer
import Combine
class stackoverflowanswerTests: XCTestCase {
var model: TestableModel!
override func setUp() {
model = TestableModel()
}
func testValidation() throws {
let expectationSuccessfulValidation = self.expectation(description: "waiting successful validation")
let expectationFailedValidation = self.expectation(description: "waiting failed validation")
let subscriber = model.$isValid.sink { _ in
// look at the output. at the first time there will be "nothing"
print(self.model.username == "" ? "nothing" : self.model.username)
if self.model.username == "1234" {
expectationSuccessfulValidation.fulfill()
} else if self.model.username == "1" {
expectationFailedValidation.fulfill()
}
}
model.username = "1234"
wait(for: [expectationSuccessfulValidation], timeout: 1)
XCTAssertTrue(model.isValid)
model.username = "1"
wait(for: [expectationFailedValidation], timeout: 1)
XCTAssertFalse(model.isValid)
}
}
Run Code Online (Sandbox Code Playgroud)
这是输出
2020-01-14 09:16:41.207649+0600 stackoverflowanswer[1266:46298] Launching with XCTest injected. Preparing to run tests.
2020-01-14 09:16:41.389610+0600 stackoverflowanswer[1266:46298] Waiting to run tests until the app finishes launching.
Test Suite 'All tests' started at 2020-01-14 09:16:41.711
Test Suite 'stackoverflowanswerTests.xctest' started at 2020-01-14 09:16:41.712
Test Suite 'stackoverflowanswerTests' started at 2020-01-14 09:16:41.712
Test Case '-[stackoverflowanswerTests.stackoverflowanswerTests testValidation]' started.
nothing
~~~> true
1234
~~~> false
1
Test Case '-[stackoverflowanswerTests.stackoverflowanswerTests testValidation]' passed (0.004 seconds).
Test Suite 'stackoverflowanswerTests' passed at 2020-01-14 09:16:41.717.
Executed 1 test, with 0 failures (0 unexpected) in 0.004 (0.005) seconds
Test Suite 'stackoverflowanswerTests.xctest' passed at 2020-01-14 09:16:41.717.
Executed 1 test, with 0 failures (0 unexpected) in 0.004 (0.005) seconds
Test Suite 'All tests' passed at 2020-01-14 09:16:41.718.
Executed 1 test, with 0 failures (0 unexpected) in 0.004 (0.006) seconds
Run Code Online (Sandbox Code Playgroud)
更新2实际上,如果我更改了这行代码,我确实会发现“异步等待失败:... ”的错误:
2020-01-14 09:16:41.207649+0600 stackoverflowanswer[1266:46298] Launching with XCTest injected. Preparing to run tests.
2020-01-14 09:16:41.389610+0600 stackoverflowanswer[1266:46298] Waiting to run tests until the app finishes launching.
Test Suite 'All tests' started at 2020-01-14 09:16:41.711
Test Suite 'stackoverflowanswerTests.xctest' started at 2020-01-14 09:16:41.712
Test Suite 'stackoverflowanswerTests' started at 2020-01-14 09:16:41.712
Test Case '-[stackoverflowanswerTests.stackoverflowanswerTests testValidation]' started.
nothing
~~~> true
1234
~~~> false
1
Test Case '-[stackoverflowanswerTests.stackoverflowanswerTests testValidation]' passed (0.004 seconds).
Test Suite 'stackoverflowanswerTests' passed at 2020-01-14 09:16:41.717.
Executed 1 test, with 0 failures (0 unexpected) in 0.004 (0.005) seconds
Test Suite 'stackoverflowanswerTests.xctest' passed at 2020-01-14 09:16:41.717.
Executed 1 test, with 0 failures (0 unexpected) in 0.004 (0.005) seconds
Test Suite 'All tests' passed at 2020-01-14 09:16:41.718.
Executed 1 test, with 0 failures (0 unexpected) in 0.004 (0.006) seconds
Run Code Online (Sandbox Code Playgroud)
对此,正如 Xcode 所建议的:
let subscriber = model.$isValid.sink { _ in
Run Code Online (Sandbox Code Playgroud)