下面的代码应该怎么写测试用例?
How should write the test case for the code below?
如果这段代码不适合编写测试代码,应该如何修改编写测试用例的代码?
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
}
}
考虑将 class 的方法提取到一个单独的 protocol
中,这样我们就可以使实际的 class 和模拟的 class 都符合该协议,我们可以在单元测试中测试预期的功能,而不是在实际实现中执行代码。
/*
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
class也可以通过依赖注入获取依赖
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
}
}
随意将上述所有 classes 和单元测试复制粘贴到单独的 playground 文件中。要 运行 Playground 中的两个单元测试,写 -
TestSomeViewModel.defaultTestSuite.run()
其他一些注意事项:-
- 建议先编写单元测试,运行 看看它是否失败,然后编写通过单元测试所需的最少代码量。这叫做Test Driven Development.
- 如果所有实现 classes 使用依赖注入,编写测试会更容易。
- 考虑避免使用单例。如果不小心使用单例,它们会使代码难以进行单元测试。欢迎阅读更多关于为什么我们应该谨慎使用单例的内容 here and here。
如果这段代码不适合编写测试代码,应该如何修改编写测试用例的代码?
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
}
}
考虑将 class 的方法提取到一个单独的 protocol
中,这样我们就可以使实际的 class 和模拟的 class 都符合该协议,我们可以在单元测试中测试预期的功能,而不是在实际实现中执行代码。
/*
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
class也可以通过依赖注入获取依赖
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
}
}
随意将上述所有 classes 和单元测试复制粘贴到单独的 playground 文件中。要 运行 Playground 中的两个单元测试,写 -
TestSomeViewModel.defaultTestSuite.run()
其他一些注意事项:-
- 建议先编写单元测试,运行 看看它是否失败,然后编写通过单元测试所需的最少代码量。这叫做Test Driven Development.
- 如果所有实现 classes 使用依赖注入,编写测试会更容易。
- 考虑避免使用单例。如果不小心使用单例,它们会使代码难以进行单元测试。欢迎阅读更多关于为什么我们应该谨慎使用单例的内容 here and here。