Build a Foursquare clone iOS app — Part 2: Location data and managing dependencies
- Part 1: Introduction and setup
- Part 2: Location data and managing dependencies
- Part 3: Continuous Integration
- Part 4: Streaming location
- Part 5: Network layer
- Part 6: State management
Location data source
As mentioned in the first part, the app will rely on user location to display its nearby interesting places. Unfortunately, the iOS native location API isn’t simple as I would want, so I’ve decided to wrap this logic into another class called
Since the main goal is to achieve high testability with our code, the dependency of the wrapper class (the CLLocationManager provided by iOS SDK) won’t be instantiated by the wrapper class itself, but passed externally by whoever needs to use the wrapper. Personally I like to use the
init (constructor) method for this.
This way you also make it explicit to another developer which are the dependencies of this class.
UserLocationService wrapper has to inherit
NSObject to conform with
NSObjectProtocol and also conform with
CLLocationManagerDelegate protocol. Otherwise we could use a
struct instead of a
class, and enjoy the benefits of immutability.
But even by using dependency injection, we still can’t easily substitute the real dependency by a fake/mock when testing. Ideally the dependency would need to be an interface in an objected-oriented language, and we can actually achieve this by using the Protocol-Oriented Programming concept.
Procotol-Oriented Programming (POO) to the rescue
We will use this Swift feature to tell the compiler that we want the native CLLocationManager class to conform to a protocol declared at our project:
This allow us to make our wrapper depends on an abstraction (protocol) instead of an implementation (dependency inversion principle), and easily mock the CLLocationManager behavior.
UserLocationService receiving an interface as a dependency (instead of receiving the implementation
CLLocationManager) will look like this:
As you can notice, this class will be responsible for receiving the location updates and will provide this information for whoever needs. When testing we can pass a mock implementation in the
constructor method, and the real
Dependency injection responsibility
Until now, the
UserLocationService class has been removed from the responsibility of instantiating its own dependency, but that role still needs to be done to whoever instantiates the wrapper class.
A dependency injection framework (Swinject in this case) can manage this responsibility and also switch between the real and mock implementations accordingly. Let’s see how this works in the first test case, but before we need an initial mock implementation of
LocationManager protocol to use in the tests:
Configuring the container
A container in the Swinject library is a class that we can use to register all dependencies of our application and how we want it to be instantiated. We will use two types of containers in this app, one for the test environment (which will contain the mocks) and one for the common application (containing the real implementations).
Now, we will start writing the tests for the new
UserLocationService class, starting by creating a test container and registering its dependencies:
In lines 11–12 we are instructing the container to use an instance of
LocationManagerMock when someone asks for a
LocationManager type. We had to configure the
container object scope so we can have access this instance later in the tests.
In lines 13–14 when resolving the
UserLocationService dependency, we are telling the container to use the previously registered
I’ve decided to exceptionally use the force unwrap here because I’ve also written separate tests for the dependency resolution, ensuring all the dependencies are registered correctly.
First test cases
When elaborating test cases, I’d like to use a similar technique described in the book Working Effectivelly with Legacy Code by Michael Feathers, called “Telling the Story of the System”. In the book, this technique is used to communicate a general overview of a software. So by using this techinique, when writing test cases I like to ask myself: “What would I want
UserLocationService to do?” and then answer in a storytelling format:
- I’d like the delegate of
LocationManagerto be an
- I’d like
UserLocationServiceto ask for user location permission when asked for coordinates.
Given our classes has been already designed to be tested, we only need to convert the test cases above into code:
These initial test cases already guarantees a working foundation for the app. It may seem obvious cases and not adding real value, and it’s probably because we didn’t follow the TDD baby steps (red-green-refactor) which would made the test cases much more intuitive.
Now that we have some working code we can setup a Continuous Integration environment to keep validating the current tests and all future commits.