В реальной жизни

Для большего контроля над зависимостями и упрощения тестирования DIP предлагает использовать инверсию управления (Inversion of Control, IoC) и инъекцию зависимостей (Dependency Injection, DI).

DIP, DI и тестирование

Писать тесты без DIP возможно, но следуя принципу это делать проще. Если модуль зависит от абстракции, то его зависимость легче подменить фейковым объектом.

Вернёмся к примеру с аутентификацией. Для того, чтобы протестировать реализацию, которая не следует DIP, нам придётся создать экземпляр класса MySqlConnection:

describe('Auth', () => {
  let connection: MySqlConnection
  let auth: Auth

  beforeEach(() => {
    connection = new MySqlConnection(/*...*/)
    auth = new Auth(connection)
  })

  it('should successfully authenticate user', async () => {
    const result: AuthResult = await auth.authenticate('Alex', '123')
    expect(result.status).toEqual(200)
  })
})

Проблема — в производительности. Хороший модульный тест должен быть быстрым, а создавать экземпляр настоящего объекта на каждый тест — ресурсозатратно.

Кроме того, в плохой реализации класс MySqlConnection сам может зависеть от других модулей, экземпляры которых тоже будут создаваться. Всё это сильно тормозит тесты.

Теперь попробуем протестировать реализацию, которая следует DIP:

class DbMock implements DataBaseConnection {
  connect(host: string, user: string, password: string): void {
    // Здесь дешёвые операции-заглушки,
    // подключаться к базе совсем не обязательно.
  }

  // Тут реализация только тех методов,
  // которые понадобятся для тестов.
}

describe('Auth', () => {
  let connection: DataBaseConnection
  let auth: Auth

  beforeEach(() => {
    connection = new DbMock(/*...*/)
    auth = new Auth(connection)
  })

  // ...
})

Класс DbMock реализует интерфейс DataBaseConnection. Мы можем спроектировать его максимально простым и лёгким, это ускорит работу теста.

Инверсия управления

DI — это частный случай инверсии управления. При этом подходе контроль за выполнением программы отдаётся фреймворку, который знает, в какой момент и какую функцию надо вызвать. Цель IoC — сделать систему расширяемой.

Inversify предлагает решение для IoC на TypeScript. Inversify предоставляет API для создания контейнеров с указанием зависимостей, которые потом подставляются автоматически.

Материалы к разделу