我正在尝试使用 TDD 构建一个在搜索之前进行反跳的搜索视图模型。直到最近,我还创建了一个模拟去抖动器,将其注入到我的视图模型中,并用它来控制测试。然而,我试图通过只让测试尽可能多地测试外部实现来使测试不那么脆弱(因此,如果我想重构,则与我选择的去抖器的关系较小)。
这是我目前的尝试,我有两个问题:
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())
}
}
提前非常感谢。
我的建议是
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
}
}
}