Using the dependency injection framework for testing in Vapor 3 and Swift

April 10, 2019

In my previous blogpost, How to test controllers by mocking dependencies in Vapor 3 and Swift, I injected dependencies into the controller using a constructor, but it turns out that there is a better way to do it. Vapor 3 introduces a dependency injection framework, which allows you to register, configure, and create your application’s dependencies in a maintainable way.

Show me the code ☀️

Why should you use the dependency injection framework? 🤔

I was a little skeptical when I first read about it in the documentation. My main concern was that it would be hard to mock inside the tests. It turns out that it’s not.

The Service (vapor/service) framework provides you with a couple of interesting features:

  • Easy access to the collection of registered services
  • Configure preferences for certain services over others
  • Initialize one object and reuse it everywhere or create a new one for each call
  • Register different services depending on the environment

Creating a new service in your controller is very simple, just call .make(_:) on the request and pass the type you want.

let availabilityChecker = try request.make(AvailabilityCheckerProtocol.self)

You have to remember that the class that conforms to registered protocol has to conform to the Service protocol. You can achieve this by creating extension:

extension AvailabilityChecker: Service {}

The service protocol is just an empty protocol for declaring types that can be registered as a service.

How to use it inside the tests? 🛠

The key to this is using protocols instead of the concrete implementation.

If you create a service using the class that implements your protocol, for example, like this:

let availabilityChecker = try request.make(AvailabilityChecker.self)

Inside the tests it will be impossible to switch AvailabilityChecker for a mock object like AvailabilityCheckerMock.

So initializing the service as a protocol allows you to register different implementations for the application and for testing.

For the application, your configure.swift file will look like this:

services.register(AvailabilityCheckerProtocol.self) { container in
  return AvailabilityChecker()
}

For testing, when you will use Vapor ➡️ helper, you can simply register mock in the configure closure like this:

final class AvailabilityTests: XCTestCase {
  
  var app: Application?
  
  override func setUp() {
    super.setUp()
    app = try! Application.makeTest(configure: { (config, services) in
      services.register(AvailabilityCheckerProtocol.self) { container in
        return AvailabilityCheckerMock()
      }
    }, routes: testRoutes)
  }

Source code with those examples is available here: ⚙️Mocking-Dependencies-Vapor3.

Summary ✅

As you can see, when creating services it’s much better to use protocols over the concrete implementation. When using protocols it’s very easy to mock all of the dependencies inside tests.

It’s definitely worth playing around and experimenting with the service framework. If you haven’t yet you should give it a try 👍

What do you think? Did you have any issues with testing while using services? Let me know - along with your questions, comments or feedback - on Twitter @mikemikina

Happy coding 😊