如何测试去抖逻辑

问题描述 投票:0回答:1

我正在尝试使用 TDD 构建一个在搜索之前进行反跳的搜索视图模型。直到最近,我还创建了一个模拟去抖动器,将其注入到我的视图模型中,并用它来控制测试。然而,我试图通过只让测试尽可能多地测试外部实现来使测试不那么脆弱(因此,如果我想重构,则与我选择的去抖器的关系较小)。

这是我目前的尝试,我有两个问题:

  1. 有没有办法可以测试这个而不必等待指定的时间?通过只允许自己测试外部,如果不使用实际的等待,我就看不到前进的方向。
  2. 如果需要使用计时器,则将测试中的延迟设置为
    UInt64(0.21 * 1_000_000_000)
    是不稳定的:有时 0.21 秒足够长,有时则不够,我需要将等待时间增加到 0.22。我对此并不热衷,因为那时我还没有真正测试去抖时间。谁能解释一下为什么 0.21 不起作用?
import XCTest
import UIKit
@testable import SearchViewModel

class SearchViewModelTests: XCTestCase {

    func test_whenSearchingForString_networkRequestIsFiredAfterGivenTime() async throws {
        let spyNetworkService = SpyUrlSession()
        let sut = SearchViewModel(searchApiService: spyNetworkService, searchDebounce: 0.2)
        sut.search(for: "My test string")
        XCTAssertEqual(spyNetworkService.dataTaskCallCount, 0, "The request should not have fired immediately")

        // Wait for debounce time to elapse
        let delay = UInt64(0.22 * 1_000_000_000)
        try await Task.sleep(nanoseconds: delay)

        XCTAssertEqual(spyNetworkService.dataTaskCallCount, 1)
    }
}

protocol URLSessionProtocol {
    func data(for request: URLRequest) async throws -> (Data, URLResponse)
}

struct SearchViewModel {
    
    let urlSession: URLSessionProtocol
    let searchDebounce: Double
    
    init(searchApiService:  URLSessionProtocol = URLSession.shared, searchDebounce: Double) {
        self.urlSession = searchApiService
        self.searchDebounce = searchDebounce
    }
    
    func search(for searchTerm: String) {
        Task {
            try? await Task.sleep(nanoseconds: UInt64(searchDebounce * 1_000_000_000))
            
            guard let url = URL(string: "https://www.my-search-service/\(searchTerm)") else {
                print("Invalid URL")
                return
            }
            
            let request = URLRequest(url: url)
            do {
                let (_, _) = try await urlSession.data(for: request)
            } catch {
                print("Request failed with error: \(error)")
            }
        }
    }
}

extension URLSession: URLSessionProtocol { }

class SpyUrlSession: URLSessionProtocol {
    var dataTaskCallCount = 0
    var lastSentRequest: URLRequest?
    func data(for request: URLRequest) async throws -> (Data, URLResponse) {
        dataTaskCallCount += 1
        lastSentRequest = request
        return (Data(), URLResponse())
    }
}

提前非常感谢。

swift unit-testing tdd debouncing
1个回答
0
投票

我的建议是

wait
在固定的时间内,您可以检查从
search
函数开始到调用 api 的持续时间:

class SearchViewModelTests: XCTestCase {

    func test_whenSearchingForString_networkRequestIsFiredAfterGivenTime() async throws {
        let spyNetworkService = SpyUrlSession()
        let debounceTime: Double = 0.2
        let sut = SearchViewModel(searchApiService: spyNetworkService, searchDebounce: debounceTime)
        sut.search(for: "My test string")
        XCTAssertEqual(spyNetworkService.dataTaskCallCount, 0, "The request should not have fired immediately")
        let timeStart = Date().timeIntervalSince1970
        await spyNetworkService.waitWhenCall(for: debounceTime + 1)
        let timeFinish = Date().timeIntervalSince1970

        XCTAssertEqual(spyNetworkService.dataTaskCallCount, 1)
        XCTAssert(timeFinish - timeStart > debounceTime)
    }
}

protocol URLSessionProtocol {
    func data(for request: URLRequest) async throws -> (Data, URLResponse)
}

struct SearchViewModel {
    
    let urlSession: URLSessionProtocol
    let searchDebounce: Double
    
    init(searchApiService:  URLSessionProtocol = URLSession.shared, searchDebounce: Double) {
        self.urlSession = searchApiService
        self.searchDebounce = searchDebounce
    }
    
    func search(for searchTerm: String) {
        Task {
            try? await Task.sleep(nanoseconds: UInt64(searchDebounce * 1_000_000_000))
            
            guard let url = URL(string: "https://www.my-search-service/\(searchTerm)") else {
                print("Invalid URL")
                return
            }
            
            let request = URLRequest(url: url)
            do {
                let (_, _) = try await urlSession.data(for: request)
            } catch {
                print("Request failed with error: \(error)")
            }
        }
    }
}

extension URLSession: URLSessionProtocol { }

class SpyUrlSession: URLSessionProtocol {
    var dataTaskCallCount = 0
    var lastSentRequest: URLRequest?
    
    private var continuation: CheckedContinuation<Void, Never>?
    func data(for request: URLRequest) async throws -> (Data, URLResponse) {
        dataTaskCallCount += 1
        lastSentRequest = request
        continuation?.resume(returning: ())
        continuation = nil
        return (Data(), URLResponse())
    }
    
    func waitWhenCall(for time: Double) async {
        //race with actual API call to check case when API not get called
        Task.detached {
            try? await Task.sleep(nanoseconds: UInt64(time * 1_000_000_000))
            self.continuation?.resume(returning: ())
            self.continuation = nil
        }
        await withCheckedContinuation { [weak self] continuation in
            guard let self = self else { return }
            self.continuation = continuation
        }
    }
}
© www.soinside.com 2019 - 2024. All rights reserved.