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.
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.
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.
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.
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.
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.
Top comments (0)