I’m doing some rapid prototyping of a TypeScript API that will query a database at some point. I quickly ran into coupling issues when I went to write some unit tests.

Dependency Injection enters the chat...

Dependency Injection?

Dependency injection describes the process of injecting dependent classes into classes such that we enforce the “Dependency Inversion Principle”. Our cody will rely on interfaces instead of relying on (and being coupled to) specific implementations.

In this example, I leverage dependency inversion (and dependency injection) to rely on a database driver interface instead of a MySQL database driver implementation. This enables me to be able to quickly swap out database drivers without rewriting any code.

Example

This is a simple API application. My layers:

  • API (fronted by something like AWS Gateway)
  • Service that serves as the middle layer between the API and the database
  • Database “driver” that executes queries and returns data or insert/update results

I explored a few dependency injection frameworks like Microsoft’s Tsyringe and TypeDI. I chose TypeDI because I had an easier time getting it bootstrapped.

API Layer

import 'reflect-metadata';
import Container from 'typedi';
import { PersonService } from '../service/person-service';

export async function getById(event, context) {
  // get my person service from the TypeDI Container
  const service: personService = Container.get(PersonService);
  const person = await personService.getById(event.id);

  return {
    'statusCode': 200,
    'body': JSON.stringify(person);
  }
}

I use the Container import to retrieve an instance of the PersonService. I annotated it with @Service so it’s automatically injected when it’s needed. Great!

Service Layer

@Service
export class PersonService {
  @Inject('db') private db: DatabaseDriver

  constructor() { }

  async getById(id: number): Promise<Person> {
    const row = await this.db.query('SELECT id, name FROM person WHERE id = ?', id);
    return toPerson(row);
  }
}

I annotated PersonService with @Service. I also leveraged the @Inject annotation to tell TypeDI to inject an instance of the DatabaseDriver that’s been annotated with @Service('db').

Database Layer

export interface DatabaseDriver {
  query(sql: string, ...params: any[]): Promise<any>
}

@Service('db')
export class MySqlDatabaseDriver {

  async query(sql: string, ...params: any[]): Promise<any> {
    // send the sql to MySQL, return the result set
    // ...
  }
}

Finally, I annotated my MySqlDatabaseDriver with @Service('db'). When anything injects a service with the name db, it will inject an instance of MySqlDatabaseDriver.

Now I’m at the point where I’d like to write some unit tests. I have dependency injection set up so this becomes a really easy task.

import 'reflect-metadata';
import { suite, test } from '@testdeck/mocha';
import { expect } from 'chai';
import { mock, when, anyString, anything } from 'ts-mockito';
import { PersonService } from '../src/service/person-service';
import { DatabaseDriver } from '../src/database/database-driver';
import Container from 'typedi';

@suite class PersonServiceTest {

  private serviceUnderTest: PersonService;
  private mockDb: DatabaseDriver;

  before() {
    // replace my inject @ `db` with a mocked database driver
    this.mockDb = mock<DatabaseDriver>();
    Container.set('db', mockDb);
    this.serviceUnderTest = Container.get(PersonService);
  }

  @test 'Service should return person with id = 1'() {
    const row = [{ id: 1, name: 'Person McPerson' }]
    
    when(this.mockDb.query(anything(), anything())).thenResolve(row);

    const person = await serviceUnderTest.getById(1);

    expect(person.id).to.equal(1);
    expect(person.name).to.equal('Person McPerson');
  }
}

I am able to code to an interface, DatabaseDriver, instead of MySqlDatabaseDriver. This allows me to avoid all the unncessary details of a MySQL database implementation (like connections, pools, etc.) in my unit tests. This also allows my PersonService to avoid all of those details as well.

That’s it. It’s a bit more complex than I make it out to be. If you are interested in a complete implementation, check out the great documentation at TypeDI.

🧇