Build a Foursquare clone iOS app — Part 2: Location data and managing dependencies
Content
- 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 UserLocationService
.
Dependency management
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.
The 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 CLLocationManager
otherwise.
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 registeredLocationManager
.
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
LocationManager
to be anUserLocationService
instance. - I’d like
UserLocationService
to 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:
Conclusion
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.