Better Testing with Dependency Injection
Tags: testing, python, ruby, typescript
Published: 2021-02-04 11:07:03

I prefer TDD for development. But in many cases, it's too hard to write tests when we use external services.


## Problem

Let's imagine we have a model that we use for caching any hashes to a store, e. g. Redis. We will use python and pytest.

```python
class Cache(Model):
    key: str
    data: dict
```

We need to implement a simple repository to manage caching to the storage. It can look like so.

```python
class CacheRepository:
    def save(self, cache):
        return storage.set(cache.key, cache.json())


def test_saves_cache_to_storage():
    cache = CacheFactory()
    repo = CacheRepository()

    repo.save(cache)

    assert storage.get(cache.key) == cache.json()
```


So. It looks like we have an extra thing to remember in our test environment, say, a local computer or CI container.  We need to initialize storage before tests and clean up and close it after. And of course, waste time to connect, no matter how fast it is. 

We have lots of packages that can help us to mock Redis using pytest. But the easiest way is framework agnostic. It's the usage of Dependency Injection.


## Solution with DI

What Dependency Injection is. The simplest explanation of DI is a case when we instead of using an initialized instance of dependency, pass it as argument.

Look, how we can implement the same code using DI. It seems like the previous version. But now we have more space for dealing with storage.


```python
class CacheRepository:
    def __init__(self, storage=storage):
        self.storage = storage

    def save(self, cache):
        return self.storage.set(cache.key, cache.json())


class MockStorage:
    _storage = {}

    def set(self, key, value):
        self._storage[key] = value

    def get(self, key):
        return self._storage.get(key)


def test_saves_cache_to_storage():
    storage = MockStorage()
    cache = CacheFactory()
    repo = CacheRepository(storage=storage)

    repo.save(cache)

    assert storage.get(cache.key) == cache.json()
```


Now we don't depend on storage in our unit tests. We won't waste time for connection, and won't think about how to set up and clean up storage around tests.

When we write integration tests we have to use real storage. But it is another story. Using DI we can check business logic with lots of fast unit tests and run only a few slow tests to see how the system works together.


## Examples from other languages

With ruby and rspec. To get a link for a telegram file using a bot we have to provide a bot's token in a link. Instead of revealing our credentials in the testing suite, we can just provide it throw DI.

```ruby
class TelegramFile
  def initialize(bot:, file_id:)
    @bot = bot
    @file_id = file_id
  end

  def link
    "https://api.telegram.org/file/bot#{bot_token}/#{file_path}"
  end

  private

  attr_reader :bot, :file_id

  def file_path
    file.dig("result", "file_path")
  end

  def file
    bot.get_file(file_id: file_id)
  end

  def bot_token
    bot.token
  end
end

describe TelegramFile do
  describe "#link" do
    it "returns file link" do
      bot = instance_double("Telegram::Bot::Client", token: "_123", get_file: file_response)
      file = described_class.new(bot: bot, file_id: "file_id")

      expect(file.link).to eq "https://api.telegram.org/file/bot_123/photos/file_5.jpg"
    end
  end
end
```

With typescript and jest. We implement a simple helper that says includes site link necessary utm marks. As we have no `document.location.search` in our test environment, we can just pass it using DI.

```typescript
export class UtmMatcherService {
  constructor(
    private combinations: Record<string, string>[],
    private search: string = document.location.search,
  ) {}

  hasMatches = (): boolean => {
    return this.includesAtLeastOneOfCombination()
  }
}

describe('UtmMatcherService', () => {
  describe('#hasMatches', () => {
    const combinations: Record<string, string>[] = [
      {utm_source: 'stories', utm_medium: 'mobile'},
      {utm_source: 'facebook'},
    ]

    context('when we have one suitable utm', () => {
      const search = '?utm_source=facebook'

      it('returns true for one utm', () => {
        const matcher = new UtmMatcherService(combinations, search)

        expect(matcher.hasMatches).toBe(true)
      })
    })
  })
})
```


## TLDR
Dependency Injection can do our tests faster. It is a simple and framework agnostic method. Instead of setup and checking external services we can just use their mocks throw arguments.