Returning to An Old Project: Part 2

 

Returning to An Old Project: Part 2

The logical place to start reviewing and fixing up my Devonian app is in the area of managing the database.  I have one class that works on both iOS and MacOS that handles this job.   Reviewing the class… it’s quite a bit of code: well over 800 lines.  Individual classes with high line counts are not that unusual, especially for things like view controllers (if you stick to the classic Model-View-Controller, MVC, design pattern).  However, I’m not happy about that.  I want it to be smaller, leaner, easier to manage, and finally make it easier to transition to another file format or web service for the database.

There were a few tests.  Most failed.  After perusing the tests I discovered a major problem: I didn’t understand what the class actually did.  Tests serve as a way not only make sure your code works, but to do things like ensure contracts with the rest of your code are respected, and, quite importantly, they document the code.  The tests failed in this regard.

Part of the problem with the tests is that I didn’t understand how to write them effectively.  I probably still don’t always write them effectively, but I’m better 2 years on.  Furthermore, since I really didn’t understand how to mock code in Swift, the class was almost impossible to test anyway - mocks would be required to do unit tests.  So, what kind of tests did I have in the end?  Integration tests.  These tests had to use real data and real database in order to work and the database was likely to change over time.

So, chuck the tests and start over.

Now, what to test.  Generally, I want to exercise  all the code in my class.  But, since I wrote this for myself, I made no distinction between public and private methods and variables.  Anything that should be private should not be tested directly, since those things should impact other calls.  In addition, it was worth eliminating any dead code (if I really needed it back, I use version control like any sane developer).   I found a bit more of this stuff as testing went on, so it’s okay to eliminate stuff later.

Finally, it was time to slog through backfilling the tests.  Keep in mind, at this stage I’m only testing to suss out the contract this class is using to interface with the rest of the app, and not make meaningful rewrites or refactors.  So, the goal is to be minimally invasive.   The main exceptions (other than dead  code removal and public/private) include making the code easier to understand and easier to test.

As far as understanding, I had a number of values that were essentially magic numbers: i.e. numbers that seemingly come from nowhere.  Fortunately, I knew what the numbers meant, so I could assign them as variables or enums.  I would have trouble understanding what 832 might be, but if I passed the value around as “George”, it can make more sense.  If the code makes more sense, it will be easier to test.

Going through the testing process quickly brought up the need for mock classes.  In particular, mocks for FileManager and NotificationCenter.  Both of these standard Apple classes are commonly using on Apple platforms.  Most apps will use these somehow.  However, they are both essentially singletons: they both are single objects in your app that you can call from almost anywhere.  Very  convenient, but very hard to test when you do so.

First, the FileManager handles files: creates, deletes, verifies it’s there, etc.  Makes sense to use.   However, all sorts of things can happen while doing these operations.  For example, your code might do X if a file exists or do Y if it doesn’t.  Imagine for a moment your app will ..usually.. successfully create a file.  It almost never fails.  Maybe once in a blue moon something happens but it’s fairly consistent.  How do you test the failure state?  Your file is always created, right?  What if you could get the FileManager to say, “sorry, there’s no file there?”  You need a mock for that.

I’m going something bad and refer to anything that isn’t a real object as a mock: so I’m going to conflate mocks, fakes, and stubs.  Mock are objects that stand in for real objects and track what’s been called on them.  You can these assert that what you expect was called, was indeed called.  However, these objects are essentially non-functioning.   Fakes on the other hand are functioning objects that typically function similarly to the real object but how it works is simplified.  Stubs are similar to mocks but can respond with data.   So if you call a method on a real object that returns a value, a stub can do the same thing with canned responses.  Of the three, mocks and stubs are very similar and, in my usage, are best mixed.  You want to see what’s called, and if I response is required, then  do so.

So, we need a mock(with stubs) FileManager.  But how?  Swift makes it tricky given that it’s type safe.  Indeed, how to we deal  with those singleton calls?  How are those replaced with mocks?  It’s easier than it sounds.

Step 1.  Create a protocol in you app for the FileManager, say “MyFileManagerProtocol”.  In Swift, it will look a bit like this:

protocol MyFileManagerProtocol {

}

Then all you need is to make FileManager conform to that protocol with an extension:

@objc extension FileManager: PTFileManager {

}

Now, Apple’s FileManager conforms to MyFileManagerProtocol.  We’re not quite done yet.  Take a look at the FileManager calls you use.  Simply add those function signatures to your own protocol:

protocol MyFileManagerProtocol {

func urls(for directory: FileManager.SearchPathDirectory, in domainMask: FileManager.SearchPathDomainMask) -> [URL]

func fileExists(atPath path: String) -> Bool

func createDirectory(at url: URL, withIntermediateDirectories createIntermediates: Bool, attributes: [FileAttributeKey : Any]?) throws

func copyItem(at srcURL: URL, to dstURL: URL) throws

}

Since FIleManager already has these methods, it already conforms to your protocol.

Now, let’s change how we call the file manager.   Instead of calling the singleton version we want to inject our protocol version.  So, on init of the class, let’s inject it:

init(fileManager: MyFileManager = FileManager.default {

self.fileManager = fileManager

}

In the class, replace every occurrence of FileManager.default with fileManager (except for the init).  Notice in this using the standard FileManager by default.  This may not be desirable in some cases, but this should be fine.

Now, make a mock (and it will also be a stub).  Be sure to only include the mock in the unit test target, not the app.  Don’t forget to do a “@testable import” as well.

class MockFileManager: MyFileManager {

}

Of course, now you have to make it conform to the protocol.  In this case, we want to make some of these calls to be both mocks and stubs.  I’ll show one:

var fileExistsCalled = false

var returnFileExistsPath: Bool = false

var capturedPath: String = “”

func fileExists(atPath path: String) -> Bool {

fileExistsCalled = true

capturedPath = capturedPath

return returnFileExistsPath

}

What does the above do?  It allows us to verify that the method was called (if called, it sets fileExistsCalled to true).   It allows us to capture what path was used in the call (capturedPath).  Finally, it allows us to return whether or not the file exists.

So a test might look something like (pseudo code):

func test_doMyThing_givenFileDoesNotExist_ThenDoMyOtherThing() {_

// Given

let mockFileManager = MockFileManager() //create the mock manager

mockFileManager.returnFIleExistsPath = false //make sure we know it won’t find the file

let testObject = TestObject(fileManager: mockFileManager)

let expectedResultIfDoesntFileExist = failedResult

let expectedResultIfFileExists = successResult

let expectedFilePath = “/path/to/file”

// When

let result = testObject.doMyThing()

// Then

XCTAssertTrue(mockFileManager.fileExistsCalled) //make sure we called it

XCTAssertEqual(mockFileManager.capturedPath, expectedFile) //make sure we called the correct path

XCTAssertEqual(result, expectedResultIfDoesntFileExist)

}

The notification center is similar, except that you are capturing calls to the notification center.

More next time

, styled with lin.css