We write a lot of unit tests while working on the SoundCloud iOS application. Unit tests are obviously great. They’re short, they’re (hopefully) easy to read, and they give us confidence that the code we ship works as expected. But unit tests — as their name suggests — only tend to cover a single unit of code, most often a function or class. So how do we catch the bugs that exist in the interactions between classes — bugs like memory leaks?
Memory leaks in Swift can be tricky to catch. There’s the more obvious case of a non-weak delegate reference, but then there are those that are slightly more difficult to spot. Is it obvious, for example, that the following code could contain a memory leak?
final class UseCase {
weak var delegate: UseCaseDelegate?
private let service: Service
init(service: Service) {
self.service = service
}
func run() {
service.makeRequest(handleResponse)
}
private func handleResponse(response: ServiceResponse) {
// some business logic and then...
delegate.operationDidComplete()
}
}
Since the Service
is injected, there are no guarantees about how it behaves. By passing in the private handleResponse
function, which, although it isn’t necessarily immediately obvious, captures self
, we are providing the Service
with a strong reference to the UseCase
. If the Service
chooses to hold that reference — and we have no guarantee that it won’t — then we have a memory leak. But it’s not obvious from scanning the code that this could happen.
There’s already a great post by John Sundell on using unit tests to catch memory leaks in a single class. But with examples like the one above, which are so easy to miss, it’s not always clear that you need to write such a unit test. (We’re certainly not speaking from experience here. 😬)
Moreover, one of the signs of a good test suite is achieving the most valuable amount of coverage with as few – short! it doesn’t count if you put all your asserts in one giant test – tests as possible.
As Guilherme wrote in his recent post, new features in the SoundCloud iOS application are written following “clean architectural patterns” — most commonly some variation of VIPER. Most of these VIPER modules are constructed using what we call a ModuleFactory
. This ModuleFactory
takes some inputs — injected dependencies and configuration — and produces a UIViewController
, which is already hooked up to the rest of the module and can be put onto the navigation stack.
Within a given VIPER module, there can be multiple delegates, observers, and escaping closures, each of which has the potential to cause the entire screen to stick around in memory after it’s been removed from the navigation stack. As this happens, the memory footprint will grow, and the operating system may well decide to terminate the application.
So is it possible to cover as many of these potential leaks as we can while writing as few unit tests as we can? If not, this entire setup has been a huge waste of time.
The answer, as the title of this post might have suggested already, is yes. And we do this with integration testing. The goal of an integration test is to check how objects within a group interact with each other. Certainly our VIPER modules are groups of objects, and memory leaks are one form of interaction we definitely want to avoid.
Our plan is simple: We’re going to use our ModuleFactory
to instantiate the entire VIPER module. We’ll then drop the reference to the UIViewController
and make sure that all of the important parts of the module are destroyed alongside it.
The first problem we have is that, by design, we can’t easily access any part of our VIPER module aside from the UIViewController
. The only public
function on our ModuleFactory
is func make() -> UIViewController
. But what if we added another entry point just for our tests? This new method would be declared internal
, so we’d only be able to access it by @testable import
-ing the framework our ModuleFactory
lives in. It would return references to all of the most important parts of the VIPER module, which we could then hold weak references to in our test. That ends up looking like this:
public final class ModuleFactory {
// Some properties & init code, and then...
public func make() -> UIViewController {
makeAndExpose().view
}
typealias ModuleComponents = (
view: UIViewController,
presenter: Presenter,
Interactor: Interactor
)
func makeAndExpose() -> ModuleComponents {
// Set up code, and then...
return (
view: viewController,
presenter: presenter,
interactor: interactor
)
}
}
This solves the problem of not being able to directly access these objects. Obviously, this might not be perfect, but it suits our needs, so let’s move on to actually writing the test. Something like the following should work:
final class ModuleMemoryLeakTests: XCTestCase {
// We need a strong reference to the view. Otherwise, the entire
// module will be destroyed immediately.
private var view: UIViewController?
// However, we want to hold weak references to the
// other parts of the stack, so as to mimic the behavior
// of UIKit presenting our UIViewController onscreen.
private weak var presenter: Presenter?
private weak var interactor: Interactor?
// In the setUp method, we instantiate the ModuleFactory and
// call our makeAndExpose method. We need to make sure we
// don't accidentally hold on to the returned ModuleComponents
// tuple, as it contains strong references to all parts of the stack.
// Doing so would clearly interfere negatively with the test.
func setUp() {
super.setUp()
let moduleFactory = ModuleFactory(/* mocked dependencies & config */)
let components = moduleFactory.makeAndExpose()
view = components.view
presenter = components.presenter
interactor = components.interactor
}
// If the test passes, this tearDown step won't actually be necessary,
// but we should definitely be covered in case the test fails, in order to stop
// the tests themselves from leaking memory.
func tearDown() {
view = nil
presenter = nil
interactor = nil
super.tearDown()
}
func test_module_doesNotLeakMemory() {
// We start by ensuring all of the references are non-nil.
// This is required to diagnose false positives, e.g.
// if we failed to correctly assign the variables in the setUp stage.
XCTAssertNotNil(presenter)
XCTAssertNotNil(interactor)
// Now we remove our strong reference to the view.
// If everything is set up correctly, this is the only
// strong reference to the entire stack, so everything
// else should disappear alongside it.
view = nil
// Finally, we check that the weak references to the
// Presenter and Interactor instances are now nil.
// This ensures that these components, and any of their
// own subcomponents, are free of memory leaks.
XCTAssertNil(presenter)
XCTAssertNil(interactor)
}
}
So there we have it, a simple way to catch memory leaks throughout an entire VIPER module. It’s by no means perfect, and it requires some custom work for each new module we want to test, but it’s certainly a lot less work than writing individual unit tests for every possible memory leak. It also helps catch memory leaks you might not even realize are possible. In fact, after writing a few of these tests, we found we had one that we just couldn’t make pass, and after some investigation, we discovered a small memory leak in the module. (It bears repeating: 😬.)
This also gives us a starting point for writing a more general suite of integration tests for the module. After all, if we simply keep a strong reference to the Presenter
and replace the UIViewController
with a mock, then we can fake user input by calling methods on the presenter and make assertions on output by checking the mocked View
. Maybe we’ll have more on that in a future post.
For now, let’s all celebrate this step toward a memory-leak-free future. 🙌
(n.b. This post was written while listening to Whatever You Love, You Are by Dirty Three, which you can listen to on SoundCloud.)