Norway


Let’s get right to it. You need to test your code, and you need to test it often. You do a lot of manual throughout the development process, find bugs, and fix them. While this can be very beneficial, it leaves much of the code untested. When I say untested, I mean untested by you. The code will be tested at some point, it just might be by one of your users. This is where writing automated unit tests comes in, however, it is often the last thing developers do, if at all. Where do you start? How do you make this class testable? Many of these can be by using to mock our objects in testing.

When you are writing testable code, there are important characteristics your code should adhere to. First, you need to have control over any inputs. This includes any and all inputs that your class acts on. Second, you need visibility into the outputs. There needs to be a way to inspect the outputs generated by your code. Your unit tests will use the outputs to validate things are working as . Lastly, there should be no hidden state. You should avoid relying on internal system state that can affect your code’s behavior later. Using protocols can help to meet these characteristics.

with Protocols

Mocking is imitating something or someone’s behavior or actions. In automated software testing it is creating an object that conforms to the same behavior as the object it is mocking. Many times the object you want to test has a dependency on an object that you have no control over. There are several ways to mock this object which you depend on. One way is to subclass it. With this approach you can override all the methods you use in your code for easy testing, right? Wrong. Subclassing many of these objects come with hidden difficulties. Here are a few.

  • Unknown state: you don’t know if your object has any shared owners which can result in one of them mutating the expected state of your mock.
  • Unexpected behavior: A change in your superclass, or it’s superclass, can create unknown affects to your mock.
  • Some classes cannot be subclassed, like UIApplication.

Also, in structs are powerful and useful value types. Structs, however, cannot be subclassed. If subclassing is not an option, then how can the code be tested?

Protocols! In Swift, protocols are full-fledged types. This allows you to set properties using a protocol as it’s type. Protocols with testing overcome many of the difficulties that come with subclassing code you don’t own and the inability to subclass structs.

Mocking Example

In the example, you have a class that interacts with the file system. The class has basic interactions with the file system, such as reading and deleting files. For now, the focus will be on deleting files. The file is represented by a struct called MediaFile which looks like this.

struct MediaFile {
    var name: String
    var path: URL
}

The FileInteraction struct is a convenience wrapper around the FileManager that allows easy deletion of the MediaFile

struct FileInteraction {
    func delete(_ mediaFile: MediaFile) throws -> () {
        try FileManager.default.removeItem(at: mediaFile.path)
    }
}

All of this is managed by the MediaManager class. This class keeps track of all of the users media files and provides a method for deleting all of the users media. deleteAll method returns true if all the files were deleted. Any files that are unable to be deleted are put back in the media array.

class MediaManager {
    let fileInteraction: FileInteraction = FileInteraction()
    var media: [MediaFile] = []
   
    func deleteAll() -> Bool {
        var unsuccessful: [MediaFile] = []
        var result = true
        for item in media {
            do {
                try fileInteraction.delete(item)
            } catch {
                unsuccessful.append(item)
                result = false
            }
        }
        media = unsuccessful
        return result
    }
}

This code, as it stands, is not very testable. It is possible to copy some files to the directory, create the MediaManager with MediaFiles that point to them, and run a test. This, however, is not repeatable or fast. A protocol can be used to make the tests fast and repeatable. The goal is to mock the FileInteraction struct without disrupting MediaManger. To do this, create a protocol with the delete method signature and declare the FileInteraction conformance to it.

protocol FileInteractionProtocol {
    func delete(_ mediaFile: MediaFile) throws -> ()
}

struct FileInteraction: FileInteractionProtocol {
    ...
}

There are two changes to MediaManager that need to be implemented. First, the type of the fileInteraction property needs to be changed. Second, add an init method that takes a fileInteraction property and give it a default value.

class MediaManager {
    let fileInteraction: FileInteractionProtocol
    var media: [MediaFile] = []
    
    init(_ fileInteraction: FileInteractionProtocol = FileInteraction()) {
        self.fileInteraction = fileInteraction
    }

    ...
}

Now MediaManager can be tested. To do so, a mock FileInteraction type will be needed.

struct MockFileInteraction: FileInteractionProtocol {
    func delete(_ mediaFile: MediaFile) throws {
        
    }
}

Now the test class can be created.

class MediaManagerTests: XCTestCase {
    var mediaManager: MediaManager!

    override func setUp() {
        mediaManager = MediaManager(fileInteraction: MockFileInteraction())

        let media = [
            MediaFile(name: "file 1", path: URL(string: "/")!),
            MediaFile(name: "file 2", path: URL(string: "/")!),
            MediaFile(name: "file 3", path: URL(string: "/")!),
            MediaFile(name: "file 4", path: URL(string: "/")!)
        ]
        
        mediaManager.media = media
    }

    func testDeleteAll() {
        mediaManager.deleteAll()
        XCTAssert(mediaManager.deleteAll(), "Could not delete all files")
        XCTAssert(mediaManager.media.count == 0, "Media array not cleared")
    }
}

All of this looks good, except the delete method is marked as throws but is never tested to throw. To do this, create another mock that throws exceptions.

struct MockFileInteractionException: FileInteractionProtocol {
    func delete(_ mediaFile: MediaFile) throws {
        throw Error.FileNotDeleted
    }
}

Then modify the test class.

class MediaManagerTests: XCTestCase {
    var mediaManager: MediaManager!
    var mediaManagerException: MediaManager!

    override func setUp() {
        mediaManager = MediaManager(fileInteraction: MockFileInteraction())
        mediaManagerException = MediaManager(fileInteraction: MockFileInteractionException())
        
        let media = [
            MediaFile(name: "file 1", path: URL(string: "/")!),
            MediaFile(name: "file 2", path: URL(string: "/")!),
            MediaFile(name: "file 3", path: URL(string: "/")!),
            MediaFile(name: "file 4", path: URL(string: "/")!)
        ]
        
        mediaManager.media = media
        mediaManagerException.media = media
    }
    
    func testDeleteAll() {
        XCTAssert(mediaManager.deleteAll(), "Could not delete all files")
        XCTAssert(mediaManager.media.count == 0, "Media array not cleared")
    }
    
    func testDeleteAllFailed() {
        XCTAssert(!mediaManagerException.deleteAll(), "Exception not thrown")
        XCTAssert(mediaManagerException.media.count > 0, "Media array was incorrectly cleared")
    }
}

Summary

Initially the MediaManager delete all method was not very testable. Using a protocol to mock interaction with the file system made testing this code repeatable and fast. The same principles for testing the delete all method can be applied to other areas of interaction such as reading, updating, or moving files around. Protocols are powerful tools for testing code. They can also be used to mock Foundation classes such as URLSession and FileManager where applicable.



Source link

LEAVE A REPLY

Please enter your comment!
Please enter your name here