DEV Community

Cover image for Dependency Injection in Koa app
Sacha Clerc-Renaud
Sacha Clerc-Renaud

Posted on

Dependency Injection in Koa app

Injecting dependencies can be very useful when you work on complex project. Here we will see how I use this pattern to make my tests easier to write and to be able to simulate some use cases that are complicated to reproduce in your tests like : DB failure, ECONNREFUSED error, etc...

This article is based on code samples from this repository : koa-template-samples. Himself based on the koa-template repository.

To implement the dependencies injection I use a small middleware called koa-depsi. You can look at his README.md file to see how it work.

I encourage you to clone the koa-template-samples repository to see the full source code of this example.

First we will see how to create our Koa app through a factory function.

// /lib/app.js

function createApp(dependencies = {}) {
  const app = new Koa()

  app.use(koadepsi(dependencies))

  app.use(todosRouter.routes())
  app.use(todosRouter.allowedMethods())
  return app
}

module.exports = createApp
Enter fullscreen mode Exit fullscreen mode

full code here

This factory function allow us to create a Koa app on demand with different dependency injection. Will we see later that it will be very useful to write our tests.

Now look at the route handler createTodo.

// /lib/routes/todos/createTodo.js

const controller = require('../../controllers/todos')

async function createTodoHandler(ctx) {
  const dbClient = ctx.deps.dbClient
  const todo = await controller.createTodo(ctx.request.body, dbClient)

  ctx.status = 201
  ctx.body = todo
}
Enter fullscreen mode Exit fullscreen mode

full code here

The route handler require the controller and get the connection to the database from the request context. It call the controller and pass the dbClient to the controller as an argument.

Note : It's possible to inject directly your controller into your routes if you prefer. You can implement this pattern as you like to use it.

Now we will look at the controller :

// /lib/controllers/todos/createTodo.js

async function createTodo(todoData, dbClient) {
  return await dbClient.query( todoData )
}
Enter fullscreen mode Exit fullscreen mode

full code here

Here we see that the controller is completely decouple from the database. So if you pass a real dbCLient object the database will be called. But if you inject a fake dbClient you can completely control and simulate the values returned by the database.

Let's see a test example to better understand the advantages of this pattern:

// /tests/integrations/createTodo.test.js    

  it('Should return an error on database failure', async () => {
    const server = app({
      dbClient: {
        query: async () => {
          throw new Error('Database is down')
        },
      }, logger
    }).listen(config.app.port)

    const response = await request
      .post('http://localhost/todos')
      .send({ title: 'Lean Javascript' })
      .catch(err => err.response)

    server.close()

    assert.deepStrictEqual(response.body, {
      error: 'CREATE_TODO_ERROR',
      info: {
        done: false,
        priority: 0,
        title: 'Lean Javascript',
      },
      message: 'Something went wrong while inserting new todo: Database is down',
    }, 'Response body should be an error with error messages concatenated by verror module')
    assert.deepStrictEqual(response.status, 500, 'Status code should be 500')
  }) 
Enter fullscreen mode Exit fullscreen mode

full code here

In this tests we want to verify that our code handle properly database error's. To simulate a database error we inject a fake dbCLient in the app. This fake dbCLient will throw new Error('Database is down') when you try to perform SQL query. This allow us to easily simulate any error that our database can throw.

Notice that the factory function to create the app is very useful. You can create a new app for each test you have to do and inject the real or fake dependencies according to your tests scenarios.

To summarize :

  • Create a factory function to create your Koa app.
  • Use koa-depsi to inject your dependencies on each request context.
  • Retrieve dependencies in your route from the request context.
  • Propagate theses dependencies to your controllers.
  • Inject fake or real dependencies in your tests according to your tests scenarios.

I hope this article will be useful for some of you. I encourage you to read the full code for this example here

Let me know if you like this testing recipe in the comments or if you have ideas to improve it. :-)

PS : English is not my native language. So let me know if you find some grammar mistakes.

Top comments (1)

Collapse
 
yogeswaran79 profile image
Yogeswaran

Awesome! Thanks for sharing :)

t.me/theprogrammersclub