Alamofire 在 UIImageView 上有一个扩展,这使得加载图像变得非常容易。但是,为了对我的代码进行单元测试,我想模拟响应的结果,以便我可以测试成功和失败。我如何模拟
.af.setImage(withURL:)
函数?
示例代码:
imageView.af.setImage(withURL: url) { response in
// do stuff on success or failure
}
我认为为依赖于外部规范的代码编写测试的最干净的方法,例如 Alamofire,或者就此而言,使用 I/O,例如网络访问,是将它们的使用集中到您控制的瓶颈中,所以你可以嘲笑那个瓶颈。为此,您需要重构源代码库以使用瓶颈而不是直接调用 Alamofire。
你想要模拟的是 AlamofireImage 的
setImage(withURL:completion)
方法,所以这就是你需要为其创建瓶颈的东西。您可以创建一个 API,用于将图像从 URL 加载到视图中。由于您基本上只需要调用 Alamofire 的 API 或一些模拟,您可以使用基于继承的方法而不会遇到麻烦,但我更喜欢协议方法:
protocol ImageLoader
{
func loadImage(
into view: UIImageView,
from: URL,
imageTransition: UIImageView.ImageTransition,
completion: ((AFIDataResponse<UIImage>) -> Void)?)
}
struct AFImageLoader: ImageLoader
{
func loadImage(
into view: UIImageView,
from url: URL,
imageTransition: UIImageView.ImageTransition,
completion: ((AFIDataResponse<UIImage>) -> Void)?)
{
view.af.setImage(
withURL: url,
imageTransition: imageTransition,
completion: completion
)
}
}
这给了你调用 Alamofire 的瓶颈,但你不希望你的应用程序代码明确表示它想要使用
AFImageLoader
。相反,我们将在 UIImageView
. 的扩展中使用它
extension UIImageView
{
func loadImage(
fromURL url: URL,
using imageLoader: ImageLoader,
imageTransition: ImageTransition = .noTransition,
completion: ((AFIDataResponse<UIImage>) -> Void)? = nil)
{
imageLoader.loadImage(
into: self,
from: url,
imageTransition: imageTransition,
completion: completion
)
}
func loadImage(
fromURL url: URL,
imageTransition: ImageTransition = .noTransition,
completion: ((AFIDataResponse<UIImage>) -> Void)? = nil)
{
loadImage(
fromURL: url,
using: AFImageLoader(),
imageTransition: imageTransition,
completion: completion
)
}
}
我应该提一下,Alamofire 的实际
setImage(withURL:...)
方法实际上采用了很多具有默认值的参数。您可能应该包括所有这些,但现在我只包括imageTransition
,当然还有completion
。
请注意,您现在可以调用
myView.loadImage(from: url) { response in ... }
与使用 Alamofire 的 API 非常相似,这将使重构更容易。我选择将其命名为 loadImage
而不是 setImage
,因为在我看来,称为 set
的东西不应该进行任何网络访问以设置本地内容。 load
对我来说意味着更重量级的操作。这是个人喜好的问题。它还使差异更加突出,因此仍然直接使用 Alamofire 的代码在重构调用时将更加直观地突出loadImage(from:...)
现在让我们模拟它,这样你就可以在测试中使用它了。
struct MockImageLoader: ImageLoader
{
var responses: [URL: (UIImage?, AFIDataResponse<UIImage>)] = [:]
func loadImage(
into view: UIImageView,
from url: URL,
imageTransition: UIImageView.ImageTransition,
completion: ((AFIDataResponse<UIImage>) -> Void)?)
{
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(1))
{
let (image, response) = imageAndResponse(for: url)
if let image = image {
view.af.run(imageTransition, with: image)
}
completion?(response)
}
}
func imageAndResponse(for url: URL) -> (UIImage?, AFIDataResponse<UIImage>)
{
guard let response = responses[url] else {
fatalError("No mocked response for \(url)")
}
return response
}
mutating func add(image: UIImage, for url: URL)
{
let request = makeGetResquest(for: url)
let response = AFIDataResponse<UIImage>(
request: request,
response: nil,
data: nil,
metrics: nil,
serializationDuration: 0.0,
result: .success(image)
)
responses[url] = (image, response)
}
mutating func add(failure: AFIError, for url: URL)
{
let request = makeGetResquest(for: url)
let response = AFIDataResponse<UIImage>(
request: request,
response: nil,
data: nil,
metrics: nil,
serializationDuration: 0.0,
result: .failure(failure)
)
responses[url] = (nil, response)
}
func makeGetResquest(for url: URL) -> URLRequest {
return try! URLRequest(url: url, method: .get, headers: nil)
}
}
此时你想用它来编写测试,但你会发现你还没有完成重构你的应用程序。要明白我的意思,请考虑这个功能:
func foo(completion: @escaping (UIImage) -> Void)
{
someImageView.loadImage(fromURL: someURL)
{ response in
switch response.result
{
case .success(let image):
completion(image)
case .failure(let error):
someStandardErrorHandler(error)
}
}
}
假设你有这个测试:
func test_foo() throws
{
let expectation = expectation(description: "HandlerCalled")
var x = false
foo
{ image in
x = true
expectation.fulfill()
}
waitForExpectations(timeout: 0.001)
XCTAssertTrue(x)
}
你需要介绍你的一个
MockImageLoader
,但是写的foo
不知道。我们需要“注入”它,这意味着我们需要使用某种机制让 foo
使用我们指定的图像加载器。如果 foo
是 struct
或 class
,我们可以将它作为一个属性,但由于我将 foo
写成一个自由函数,我们将把它作为参数传入,这将与方法也。所以foo
变成:
func foo(
using imageLoader: ImageLoader = AFImageLoader(),
completion: @escaping (UIImage) -> Void)
{
someImageView.loadImage(fromURL: someURL, using: imageLoader)
{ response in
switch response.result
{
case .success(let image):
completion(image)
case .failure(let error):
someStandardErrorHandler(error)
}
}
}
这意味着当您编写使用
MockImageLoader
的测试时,您将越来越需要以某种方式传递 ImageLoader
s。对于那些部分,您可以逐步完成。
OK,现在让我们在测试中创建一个 Mock:
func test_foo() throws
{
let expectation = expectation(description: "HandlerCalled")
// You might want to use some real image here
let anImage = UIImage()
var imageLoader = MockImageLoader()
imageLoader.add(image: anImage, for: someURL)
var x = false
foo(using: imageLoader)
{ image in
x = true
expectation.fulfill()
}
waitForExpectations(timeout: 0.001)
XCTAssertTrue(x)
}
你也可以测试失败:
func test_foo_failed() throws
{
let expectation = expectation(description: "HandlerCalled")
var imageLoader = MockImageLoader()
imageLoader.add(
failure: AFIError.imageSerializationFailed,
for: someURL
)
var x = false
foo(using: imageLoader)
{ image in
x = true
expectation.fulfill()
}
waitForExpectations(timeout: 0.001)
XCTAssertFalse(x)
}