В реальной жизни
Для большего контроля над зависимостями и упрощения тестирования 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 для создания контейнеров с указанием зависимостей, которые потом подставляются автоматически.