带有内部开关盒的单元测试 API 调用

Posted

技术标签:

【中文标题】带有内部开关盒的单元测试 API 调用【英文标题】:Unit test API Call with internal switch case 【发布时间】:2020-08-13 23:40:24 【问题描述】:

我正在尝试对具有不同设置模式的类进行单元测试。

class Controller

    enum Mode
        case listing
        case pages(String?)
    

    private (set) var mode : Mode = .listing
    private (set) var models = [Model]()

    init() 
        ...
    

    init(id : String) 
        mode = .pages(id)
    

    func fetchInfo()

        switch mode
           case .listing: 
               ApiManager.firstNetworkCall() (json, error) in ... 
                    setupModel()
               
           case .pages(let id):
               ApiManager.secondNetworkCall(id : id) (json, error) in ... 
                    setupModel()
               
        
    


这两个都会用不同数量的数据更新models数组。

我现在拥有的:

var controller : Controller!

override func setUpWithError() throws 

    // Put setup code here. This method is called before the invocation of each test method in the class.
    try super.setUpWithError()
    controller = Controller()


override func tearDownWithError() throws 
    // Put teardown code here. This method is called after the invocation of each test method in the class.
    controller = nil
    try super.tearDownWithError()


func testDefaultListingMode() throws 

    switch controller.mode
       case .listing:
           XCTAssertTrue(true)
       default:
           XCTAssertFalse(false)
    


func testAPISetup() throws 

    controller.fetchInfo()
    //now what?



这会检查模式是否正确,但我试图更进一步,检查是否根据模式设置了正确数量的项目。并想直接从XCTestCase 调用fetchInfo() 方法并验证模型计数。

我看到的所有教程和指南都只是谈论用URLSession 伪造行为。但是 API 调用依赖于在 fetchInfo 方法中作为内部检查发生的模式,并且是唯一暴露给其他类的方法。我只是想测试该方法(以防该方法内部出现问题导致错误)。

我该怎么做呢?我不知道如何完成testAPISetup() 方法。

【问题讨论】:

您的控制器(及其测试)做得太多了。您的控制器的测试不应该测试任何有关 API(或它返回的内容)如何工作的内容。您应该只测试控制器是否调用了正确的 API(使用假实现)。从那里,编写单独的测试以确保 API 正常工作。 @Alexander-ReinstateMonica 我试图使用返回的数据量来测试是否调用了正确的 API。没有设置其他变量可以让我区分这两个响应。您是否建议仅使用 URLSession 单独测试 API 而忘记“模式”?因为“你应该只测试控制器调用正确的 API”这正是我坚持的部分! 啊,我知道是什么绊倒了你。不,不要使用结果来确定正在调用两个网络 API 中的哪一个(这在一般情况下不起作用,如果您有一个没有返回结果的依赖项怎么办?)。 Extract the interface 的网络客户端(我猜你可能很快将其称为“提取协议”),将您的网络客户端作为 VC 的依赖项注入,然后您的测试代码可以使用该协议的模拟实现,该协议检测哪个方法被调用 "我必须将整个 APIManager 作为参数传递。" VC 与“普通”对象有点不同,因为它们有两个阶段初始化,它们通常由系统/Storyboard 分配和最小初始化,然后从viewDidLoad 和朋友进一步“初始化”以配置您的非视图相关属性。所以你会使用类似于设置注入而不是构造函数注入的东西。您可以通过 segue 注入依赖项,或者使用默认值(您的单元测试可以覆盖) @Rikh 你刚刚发现了为什么对全局/静态成员的引用会使测试变得困难:) 【参考方案1】:

我对网络的了解:

class NetworkingManager
   static var alamoFireManager = Session.default

   static func POST(...., completion : ()->()) 
       sendRequest(....., completion : completion)
   

   private static func sendRequest(...., completion : ()->()) 
       let request = alamoFireManager.request(.....)
       request.responseJSON

           completion()
       
   


class APIManager

    static func firstNetworkCall(completion : ()->())
        NetworkingManager.POST(..., completion : completion)
    


我不得不更改上述内容并删除所有提及静态和单例的内容。我决定继续使用类继承。我试图避免它并使用协议,但坦率地说,使用类更容易!

class NetworkingManager

    private (set) var sessionManager: Session

    init(config : URLSessionConfiguration = .default)

        config.timeoutIntervalForResource = 8.0
        config.timeoutIntervalForRequest = 8.0

        self.sessionManager = Session(configuration: config)
    

    func request(...) 
         //hit alamofire
    


class APIManager : NetworkingManager

    override init(config: URLSessionConfiguration = .default) 
        super.init(config: config)
    
    
    //other methods
    ...


class Controller

    private let apiManager : APIManager
    init(manager : APIManager = APIManager())
        self.apiManager = manager
    


在我的测试课中:

override func setUpWithError() throws 

    // Put setup code here. This method is called before the invocation of each test method in the class.
    try super.setUpWithError()

    let config = URLSessionConfiguration.ephemeral
    apiManager = APIManager(config : config)
    controller = Controller(manager : apiManager)


func testApiCalled() throws

    controller.fetchNecessaryInfo()
    //had to add one second delay as alamofire adds the request on another queue. Wasn't able to put it on main queue.
    sleep(1)
    let promise = expectation(description: "Check request called")
    apiManager.sessionManager.session.getAllTasks  (taskArray) in

       if taskArray.count > 1
           XCTFail("Multiple requests when there should be only one")
       
            
       if let task = taskArray.first, let request = task.currentRequest
           if let string = request.url?.absoluteString
               XCTAssert(...)
           else
               XCTFail("Incorrect URL")
           
       else
            XCTFail("Somehow no task exists. So this is an error")
       

       promise.fulfill()
    

    wait(for: [promise], timeout: 1.0)

如果不为 APIManager 实例化一个对象,我想不出任何其他方法,所以不得不重构!

【讨论】:

以上是关于带有内部开关盒的单元测试 API 调用的主要内容,如果未能解决你的问题,请参考以下文章

如何在带有 Jest 的 react-native 中使用模拟的 fetch() 对 API 调用进行单元测试

Android:带有改造和单元测试的网络调用

软件测试流程

带有不在标题中的函数的单元测试 C

自托管 web api 并从单元测试项目中访问它

Self Host web api并从单元测试项目访问它