单元测试 SwiftUI/Combine @Published 布尔值

Unit testing SwiftUI/Combine @Published boolean values

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

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

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

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

}

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

假设您在 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 { ![=11=] }
            .receive(on: RunLoop.main)
            .assign(to: &$showOnboarding)
    }
    
    public func completedOnboarding() {
        userDefaults.set(true, forKey: "completedOnboarding")
    }
}

为了测试这个 class 你有一个 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)
}

有些测试是异步的,这意味着您需要使用 XCTExpectations 等待 @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 { ![=14=] }
        .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)
}