采取的方法是使用测试替身来检查您的upload 方法是否进行了正确的网络调用。您将对网络库进行异步调用,该库可能是URLSession,也可能是另一个库,例如 AlamoFire。使用哪个库与您的 upload 方法无关。
要实现这一点,您希望避免直接使用 URLSession,并使用符合接口的包装器,然后您可以在测试中模拟该接口。这意味着您的代码将在运行时使用与测试时不同的网络类实现,并且您将根据需要“注入”正确的实现。
例如,您的网络库可以有这个接口:
protocol NetworkRequesting {
func post(data: Data, url: URL)
}
在运行时使用以下实际实现:
struct NetworkRequester: NetworkRequesting {
func post(data: Data, url: URL) {
let session = URLSession()
let task = session.uploadTask(with: URLRequest(url: url), from: data)
task.resume()
}
}
但是,在测试时,您使用以下模拟代替:
class MockNetworkRequester: NetworkRequesting {
var didCallPost = false
var spyPostData: Data? = nil
var spyPostUrl: URL? = nil
func post(data: Data, url: URL) {
didCallPost = true
spyPostData = data
spyPostUrl = url
}
}
然后,给定下面的测试类:
class ImageUploader {
let networkRequester: NetworkRequesting
init(networkRequester: NetworkRequesting) {
self.networkRequester = networkRequester
}
func upload(image: UIImage, url: URL) {
}
}
你可以像这样测试upload的实现:
class UploadImageTests: XCTestCase {
func test_uploadCallsPost() {
let mockNetworkRequester = MockNetworkRequester()
let uploader = ImageUploader(networkRequester: mockNetworkRequester)
uploader.upload(image: UIImage(), url: URL(string:"http://example.com")!)
XCTAssert(mockNetworkRequester.didCallPost)
}
}
目前,该测试将失败,因为upload 什么都不做,但如果您将以下内容放入被测类中,则测试将通过:
func upload(image: UIImage, url: URL) {
guard let otherUrl = URL(string:"https://example.org") else { return }
networkRequester.post(data: Data(), url: otherUrl)
}
这是您的第一个 TDD 周期。显然它的行为还不是你想要的,所以你需要编写另一个测试来确保使用的 url 是你期望的,或者传递的数据是你期望的。
有很多方法可以让你的代码在运行时使用真实的网络请求者,你可以让 init 方法使用默认参数值来让它使用NetworkRequester,或者使用静态工厂方法来创建它,还有其他选项,例如控制反转,这远远超出了此答案的范围。
要记住的重要一点是,您测试的是对网络框架的正确调用,而不是测试网络框架。我喜欢让我的协议接口保持相当的声明性,传递在任何框架中发出请求所需的东西,但你可能会发现你更喜欢更接近金属并基本上反映 URLSession 的实现——这取决于你,还有更多在我看来,一门艺术而不是一门科学。