如何在 UIImageView 上模拟 Alamofire 扩展?

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

Alamofire 在 UIImageView 上有一个扩展,这使得加载图像变得非常容易。但是,为了对我的代码进行单元测试,我想模拟响应的结果,以便我可以测试成功和失败。我如何模拟

.af.setImage(withURL:)
函数?

示例代码:

imageView.af.setImage(withURL: url) { response in
    // do stuff on success or failure
}
ios swift alamofire alamofireimage
1个回答
0
投票

我认为为依赖于外部规范的代码编写测试的最干净的方法,例如 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)
    }
© www.soinside.com 2019 - 2024. All rights reserved.