Swift Regex Deep Dive
iOS MacOur introductory guide to Swift Regex. Learn regular expressions in Swift including RegexBuilder examples and strongly-typed captures.
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 testing 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 challenges can be overcome by using protocols.
Regardless of your testing methods, when you are writing testable code there are important characteristics your code should adhere to:
Using Swift mock protocols can help to meet these characteristics while also allowing for dependencies.
Mocking is imitating something or someone’s behavior or actions. In automated tests, it is creating an object that conforms to the same behavior as the real 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 create iOS unit testing mock objects. 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 comes with hidden difficulties. Here are a few.
Also, Swift 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 its type. Using protocols for testing overcomes many of the difficulties that come with subclassing code you don’t own and the inability to subclass structs.
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 MediaFile
s 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")
}
}
A Swift mock is only one type of “test double”–a test double being a replacement entity used solely for testing. There are four commonly used types of test double:
Dummy objects are empty objects used within a unit test. When you use a dummy object, you isolate the code being tested; while you can determine whether the code correctly calls the dummy, the dummy does nothing.
Stub objects are objects that always return a specific set of core data. For example, you could set an error-handling object to always return “true” if you wanted to test a failure condition, or drop a stub object into legacy code to test and isolate behavior.
Fake objects are objects that still roughly correlate to a “real” object, but are simplified for the sake of testing. Instead of pulling data from a database, you might return a specific set of data that could have been pulled from the database. Fake objects are similar to stubs, just with more complexity.
While there are use cases for dummies, stubs, and fakes–such as UI tests–mocks frequently provide more comprehensive Swift unit test data. Mocking protocols are particularly useful for dynamic environments and dependency injection. Use mocks for complex tasks such as integration tests.
One final element to discuss is the practice of partial mocking vs. complete mocking.
Complete mocking refers to mocking the entirety of the protocol, while partial mocking refers to when you override a specific behavior or behaviors in the protocol.
When unit testing, software engineers use partial mocking to drill down to specific behaviors within the protocol. Otherwise, partial and complete mocking are virtually identical–the difference lies in scope.
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. Mock protocols can also be used to mock Foundation classes such as URLSession and FileManager where applicable.
In addition to mock protocols, there are also test suites, mocking libraries, and mocking frameworks, such as Mockingbird–and the automated testing provided by continuous integration/delivery before pushing production code. Nevertheless, you should still know how to hand code your mocking and develop your own tests.
Protocols are powerful tools for testing code–and testing should never be an afterthought. Learn more about high-quality, resilient code generation and test-driven development at our iOS and Swift Essentials bootcamp.
Our introductory guide to Swift Regex. Learn regular expressions in Swift including RegexBuilder examples and strongly-typed captures.
The Combine framework in Swift is a powerful declarative API for the asynchronous processing of values over time. It takes full advantage of Swift...
SwiftUI has changed a great many things about how developers create applications for iOS, and not just in the way we lay out our...