If this code is not suitable for write test code, how should modify the code for writing test case?
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
}
}
Consider extracting the methods of the class into a separate protocol
, so that we can make both the actual class and the mock class conform to that protocol, and we can test out the intended functionalities in the unit tests instead of executing the code in the actual implementation.
/*
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))
}
}
The SomeViewModel
class can also get its dependencies via dependency injection.
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
}
}
The unit tests for the above implementation can look something similar to what is written below.
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
}
}
Feel free to copy-paste all the above classes and the unit tests to a separate playground file. To run both the unit tests in Playground, write -
TestSomeViewModel.defaultTestSuite.run()
Some other things to keep in mind :-