unit-testing - 单元测试 SwiftUI/Combine @Published bool 值

标签 unit-testing swiftui combine

我正在尝试熟悉在 SwiftUI 中对某些 View 模型进行单元测试。 View 模型当前有两个 @Published bool 值,它们在底层 UserDefaults 属性更改时发布更改。对于我的单元测试,我遵循了 this guide关于如何设置 UserDefaults 进行测试,这样我的生产值就不会被修改。我可以这样测试默认值:

func testDefaultValue() {
     XCTAssertFalse(viewModel.canDoThing)
}

我将如何着手切换 @Published 值,然后确保我的 View 模型已收到更改?因此,例如,我在我的 XCTestCase 中引用了我的模拟用户默认值。我试图以零成功执行以下操作:

func testValueTogglesToTrue() {
     defaults.canDoThing = true
     XCTAssertTrue(viewModel.canDoThing)

更新底层用户默认值的想法是将更改发布到 View 模型中的已发布值将通知我们的 View 模型。以上对 View 模型变量没有任何作用。我是否需要订阅发布者并使用接收器来完成此操作?

最佳答案

假设您在 UserDefaults 中存储了一个标志以了解用户是否已完成入职:

extension UserDefaults {

    @objc dynamic public var completedOnboarding: Bool {
        bool(forKey: "completedOnboarding")
    }
}

您有一个 ViewModel,它告诉您的 View 是否显示入职,并且有一个方法将入职标记为已完成:

class ViewModel: ObservableObject {
    
    @Published private(set) var showOnboarding: Bool = true
    
    private let userDefaults: UserDefaults
    
    public init(userDefaults: UserDefaults) {
        self.userDefaults = userDefaults
        self.showOnboarding = !userDefaults.completedOnboarding
        userDefaults
            .publisher(for: \.completedOnboarding)
            .map { !$0 }
            .receive(on: RunLoop.main)
            .assign(to: &$showOnboarding)
    }
    
    public func completedOnboarding() {
        userDefaults.set(true, forKey: "completedOnboarding")
    }
}

要测试这个类,您有一个 XCTestCase:

class MyTestCase: XCTestCase {
    
    private var userDefaults: UserDefaults!
    private var cancellables = Set<AnyCancellable>()
    
    override func setUpWithError() throws {
        try super.setUpWithError()
        userDefaults = try XCTUnwrap(UserDefaults(suiteName: #file))
        userDefaults.removePersistentDomain(forName: #file)
    }

    // ...
}

一些测试用例是同步的,例如您可以轻松地测试 showOnboarding 依赖于 UserDefaults completedOnboarding 属性:

func test_whenCompletedOnboardingFalse_thenShowOnboardingTrue() {
    userDefaults.set(false, forKey: "completedOnboarding")
    let subject = ViewModel(userDefaults: userDefaults)
    XCTAssert(subject.showOnboarding)
}
    
func test_whenCompletedOnboardingTrue_thenShowOnboardingFalse() {
    userDefaults.set(true, forKey: "completedOnboarding")
    let subject = ViewModel(userDefaults: userDefaults)
    XCTAssertFalse(subject.showOnboarding)
}

有些测试是异步的,这意味着您需要使用 XCTExpectation 来等待 @Published 值发生变化:

func test_whenCompleteOnboardingCalled_thenShowOnboardingFalse() {
    let subject = ViewModel(userDefaults: userDefaults)
    // first define the expectation that showOnboarding will change to false (1)
    let showOnboardingFalse = expectation(
        description: "when completedOnboarding called then show onboarding is false")
        
    // subscribe to showOnboarding publisher to know when the value changes (2)
    subject
        .$showOnboarding
        .filter { !$0 }
        .sink { _ in
            // when false received fulfill the expectation (5)
            showOnboardingFalse.fulfill()
        }
        .store(in: &cancellables)
        
    // trigger the function that changes the value (3)
    subject.completedOnboarding()
    // tell the tests to wait for your expectation (4)
    waitForExpectations(timeout: 0.1)
}

关于unit-testing - 单元测试 SwiftUI/Combine @Published bool 值,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/70809030/

相关文章:

python - PyBuilder - 当测试失败或覆盖率太低时如何继续?

c# - MVC - 单元测试错误的东西?

unit-testing - 如何在Dart中对Stream.listen()进行单元测试

SwiftUI 观察已发布对象的已发布对象

swift - 是否有替代Combine 的@Published 的替代方法,在它发生之后而不是之前发出值变化的信号?

swift - 为什么我的 SwiftUI 列表再次追加所有对象而不是更新现有对象?

php - 如何减少 PHPUnit 和 ZF3 测试中的数据库连接数?

ios - SwiftUI - 更改 KVKCalendar 的 CalendarType

swift - 删除核心数据对象时关闭导航 View

Swift Combine发布者与完成处理程序以及何时取消