ios - 下面的代码应该怎么写测试用例?

标签 ios swift unit-testing mobile xctestcase

如果这段代码不适合编写测试代码,应该如何修改编写测试用例的代码?

class MyFileManager {
   static let shared = MyFileManager()
 
  func isStored(atPath path: String) -> Bool {
     return FileManager.default.fileExists(atPath: path)
 }

 func readData(atPath path: String) -> Data? {
      return try? Data(contentsOf: URL(fileURLWithPath: path))
  }
}

class SomeViewModel {
  func getCachedData() -> Data? {
      let path = "xxxxx"
 
      if MyFileManager.shared.isStored(atPath: path) {
          return MyFileManager.shared.readData(atPath: path)
      } else {
          return nil
      }
  }
}

class TestSomeViewModel: XCTestCase {
  func testGetCachedData() {
      let viewModel = SomeViewModel()
      // Need to cover SomeViewModel.getCachedData() method
  }
}

最佳答案

考虑将类的方法提取到单独的 protocol 中,这样我们就可以使实际类和模拟类都符合该协议(protocol),并且我们可以在单元测试中测试预期的功能,而不是在实际实现中执行代码。

/*
    Extract the 2 methods of MyFileManager into a separate protocol.
    Now we can create a mock class which also conforms to this same protocol,
    which will help us in writing unit tests.
*/
protocol FileManagerProtocol {
    func isStored(atPath path: String) -> Bool
    func readData(atPath path: String) -> Data?
}

class MyFileManager: FileManagerProtocol {
    static let shared = MyFileManager()
    
    // To make a singleton instance, we have to make its initializer private.
    private init() {
    }
    
    func isStored(atPath path: String) -> Bool {
        //ideally, even FileManager.default instance should be "injected" into this class via dependency injection.
        return FileManager.default.fileExists(atPath: path)
    }
    
    func readData(atPath path: String) -> Data? {
        return try? Data(contentsOf: URL(fileURLWithPath: path))
    }
}

SomeViewModel 类也可以通过依赖注入(inject)获取它的依赖。

class SomeViewModel {
    var fileManager: FileManagerProtocol?
    
    // We can now inject a "mocked" version of MyFileManager for unit tests.
    // This "mocked" version will confirm to FileManagerProtocol which we created earlier.
    init(fileManager: FileManagerProtocol = MyFileManager.shared) {
        self.fileManager = fileManager
    }
    
    /*
        I've made a small change to the below method.
        I've added the path as an argument to this method below,
        just to demonstrate the kind of unit tests we can write.
    */
    func getCachedData(path: String = "xxxxx") -> Data? {
        if let doesFileExist = self.fileManager?.isStored(atPath: path),
           doesFileExist {
            return self.fileManager?.readData(atPath: path)
        }
        return nil
    }
}

上述实现的单元测试看起来类似于下面所写的内容。

class TestSomeViewModel: XCTestCase {
    var mockFileManager: MockFileManager!
    
    override func setUp() {
        mockFileManager = MockFileManager()
    }
    
    override func tearDown() {
        mockFileManager = nil
    }
    
    func testGetCachedData_WhenPathIsXXXXX() {
        let viewModel = SomeViewModel(fileManager: self.mockFileManager)
        XCTAssertNotNil(viewModel.getCachedData(), "When the path is xxxxx, the getCachedData() method should not return nil.")
        XCTAssertTrue(mockFileManager.isStoredMethodCalled, "When the path is xxxxx, the isStored() method should be called.")
        XCTAssertTrue(mockFileManager.isReadDataMethodCalled, "When the path is xxxxx, the readData() method should be called.")
    }
    
    func testGetCachedData_WhenPathIsNotXXXXX() {
        let viewModel = SomeViewModel(fileManager: self.mockFileManager)
        XCTAssertNil(viewModel.getCachedData(path: "abcde"), "When the path is anything apart from xxxxx, the getCachedData() method should return nil.")
        XCTAssertTrue(mockFileManager.isStoredMethodCalled, "When the path is anything apart from xxxxx, the isStored() method should be called.")
        XCTAssertFalse(mockFileManager.isReadDataMethodCalled, "When the path is anything apart from xxxxx, the readData() method should not be called.")
    }
}

// MockFileManager is the mocked implementation of FileManager.
// Since it conforms to FileManagerProtocol, we can implement the
// methods of FileManagerProtocol with a different implementation
// for the assertions in the unit tests.
class MockFileManager: FileManagerProtocol {
    private(set) var isStoredMethodCalled = false
    private(set) var isReadDataMethodCalled = false
    
    func isStored(atPath path: String) -> Bool {
        isStoredMethodCalled = true
        if path.elementsEqual("xxxxx") {
            return true
        }
        return false
    }
    
    func readData(atPath path: String) -> Data? {
        isReadDataMethodCalled = true
        if path.elementsEqual("xxxxx") {
            return Data()
        }
        return nil
    }
}

请随意将上述所有类和单元测试复制粘贴到单独的 playground 文件中。要在 Playground 中运行两个单元测试,请编写 -

TestSomeViewModel.defaultTestSuite.run()

其他一些要记住的事情:-

  1. 建议先编写单元测试,运行它并查看它是否失败,然后编写通过单元测试所需的最少代码量。这叫做 Test Driven Development .
  2. 如果所有的实现类都使用依赖注入(inject),编写测试会更容易。
  3. 考虑避免使用单例。如果不小心使用单例,它们会使代码难以进行单元测试。欢迎阅读更多关于为什么我们应该谨慎使用单例的信息 herehere .

关于ios - 下面的代码应该怎么写测试用例?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/68820839/

相关文章:

objective-c - 如何创建一个带有启动按钮的加载屏幕,该按钮可通往应用程序的其余部分?

ios - 优化代码物理

ios - 无法初始化 Firebase

ios - 如何在Objective-C中检查多个Switch大小写值?

ios - 如何删除警告 "Conditional cast from ' 任何'到 'AnyObject' 总是成功"

ios - 如何使用 couchbase 在 Swift 4 中通过数据库观察器检索数据?

javascript - 使用 Karma Jasmine 进行 Angular JS 测试

ios - 如何编写简单的单元测试来测试 JSON 的解析?

java - 安卓测试 : Can I send sms to the emulator number from test method?

ios - 如何调整 Core Data 默认缓存的行为或大小?