Тестовые двойники: краткое руководство по повышению навыков тестирования
Добро пожаловать в быстро развивающуюся сферу разработки программного обеспечения, где освоение Test Doubles — ваш ключ к созданию надежных программ.
Код облегчит вам путь к изучению TDD. Всего за несколько секунд мы разгадаем тайны шпионов, заглушек и фейков. Исследуя их возможности, которые помогут улучшить вашу тестовую игру независимо от того, являетесь ли вы ветераном TDD или новичком, присоединяйтесь к нам в этом быстром путешествии, которое изменит ваш подход к тестированию и повысит качество нашего кода.
Двойники вступают в игру, когда вы предпочитаете не использовать конкретную реализацию интерфейса в своих тестах, оптимизации процесса, написания тестов и ускорение обратной связи по актуальным функциям. Они оказываются ценными в обеспечении тестирования независимо от инфраструктуры, например создание двойников для репозитория и содействие более эффективному тестированию.
Фиктивные сервисы
Фиктивные сервисы — это сервисы, от которых зависит наша SUT (тестируемая система), но которые не имеют отношения к объему тестирования.
Пример с функцией создания банковского счета, использующей BankingAccountRepository, а также EventPublisher и наши текущие тесты не имеют никакого отношения к издателю событий. Мы можем просто заменить его фиктивной реализацией. Это не повлияет на нашу систему тестирования.
class CreateBankingAccount {
constructor(
private readonly _bankingAccountRepository: BankingAccountRepository,
private readonly _eventPublisher: EventPublisher
) {
}
execute() {
//Some business logic and references to _eventPublisher
}
}
interface EventPublisher {
publish(event: Event): void
}
class DummyEventPublisher implements EventPublisher {
publish(event: Event): void {
// Do nothing, we call it a dummy implementation
}
}
Шпион
Тестовый шпион — это инструмент, который перехватывает косвенные выходные данные и обеспечивает необходимые косвенные входные данные, обрабатывая выходные данные, которые непосредственно не наблюдается.
В данном фрагменте кода мы получаем подробную информацию об «опубликованных» событиях. Это особенно ценно в системе, управляемая событиями, в которой вам нужно или необходимо публиковать события в шине, еще не зная, какие последующие действия они могут предпринять.
Ключевой мотивацией использования шпиона является получение более глубокого понимания внутреннего состояния системы, даже это происходит за счет усиленной связи.
it("publishes a banking account event", () => {
const spy = new SpyEventPublisher();
const useCase = new CreateBankingAccount(bankingAccountRepository, spy);
useCase.execute();
expect(spy.events.length).toEqual(1);
expect(spy.events[0]).toEqual({id: "newBankingAccountId"});
})
class SpyEventPublisher implements EventPublisher {
private readonly _events: Array<Event> = []
publish(event: Event): void {
this._events.push(event)
}
get events() {
return this._events
}
}
class CreateBankingAccount {
constructor(
private readonly _bankingAccountRepository: BankingAccountRepository,
private readonly _eventPublisher: EventPublisher
) {
}
execute() {
//Some business logic
this._eventPublisher.publish({id: "newBankingAccountId"})
}
}
Заглушка
Заглушка — это объект, который возвращает введенные в него поддельные данные.
Рассмотрим сценарий, в котором наша функция использует внешний API для получения пользовательских данных, таких как кредитный рейтинг, необходимый для создание учетной записи. Чтобы обеспечить правильную работу службы, мы можем создать объект-заглушку с поддельными значениями.
Следующий фрагмент представляет потенциальную реализацию заглушки для внешнего API.
interface UserDataGateway {
creditScore(email: string): number
}
class StubUserDataGateway implements UserDataGateway {
private _creditScoreValue: number
creditScore(email: string): number {
return this._creditScoreValue;
}
set creditScoreValue(value: number) {
this._creditScoreValue = value;
}
}
Тогда наш тест будет выглядеть так:
it("requires a positive credit score to create a bank account", async () => {
const stub = new StubUserDataGateway();
const useCase = new CreateBankingAccount(bankingAccountRepository, new DummyEventPublisher());
{
stub.creditScoreValue = 0
useCase.execute({email: "john@doe.com"});
expect(bankingAccountRepository.accounts).toEqual([]);
}
{
stub.creditScoreValue = 20
useCase.execute({email: "jane@doe.com"});
expect(bankingAccountRepository.accounts).toEqual([/*An account object*/]);
}
})
class CreateBankingAccount {
constructor(
private readonly _bankingAccountRepository: BankingAccountRepository,
private readonly _userDataGateway: UserDataGateway
) {
}
execute({email}: { email: string }) {
if (!this._userDataGateway.creditScore(email)) return
// Bank account creation logic
}
}
Я уверен, что вы уже заметили недостаток в этой конструкции. Проблема заключается в том, что мы могли бы заменить электронную почту с любой строкой в приведенном ниже операторе из-за нашей реализации заглушки.
this._userDataGateway.creditScore(email)
Вот почему в большинстве случаев мы предпочитаем добавлять немного простой логики, а не сохранять ее полностью простой, как вы можете видеть в следующем фрагменте кода.
class StubUserDataGateway implements UserDataGateway {
private _creditScores: { [email: string]: number } = {}
creditScore(email: string): number {
return this._creditScores[email] || 0;
}
feedWith(email, creditScore) {
this._creditScores[email] = creditScore;
}
}
Ведение нашей тестовой реализации:
it("requires a positive credit score to create a bank account", async () => {
const stub = new StubUserDataGateway();
const useCase = new CreateBankingAccount(bankingAccountRepository, new DummyEventPublisher());
{
stub.feedWith("john@doe.com", 0)
useCase.execute({email: "john@doe.com"});
expect(bankingAccountRepository.accounts).toEqual([]);
}
{
stub.feedWith("jane@doe.com", 1)
useCase.execute({email: "jane@doe.com"});
expect(bankingAccountRepository.accounts).toEqual([/*An account object*/]);
}
})
Fake
И последнее, но не менее важное: фейки! Они в основном используются, когда мы хотим реализовать архитектурный интерфейс с некоторой логикой в памяти, поэтому мы не привязаны к внешней инфраструктурной службе, например, к базе данных. В зависимости от его реализации, но в большинстве случаев поддельную реализацию можно использовать в производственной среде для демонстрации целей.
Следующий фрагмент представляет собой простую, но распространенную реализацию Fake
репозитория в тривиальной системе:
class InMemoryBankingAccountRepository implements BankingAccountRepository {
private readonly _accounts: Array<BankingAccount> = []
create(bankingAccount: BankingAccount) {
this._accounts.push(bankingAccount)
}
get accounts(): Array<BankingAccount> {
return this._accounts;
}
}
it("creates an account", async () => {
const repository = new InMemoryBankingAccountRepository()
const useCase = new CreateBankingAccount(bankingAccountRepository)
useCase.execute({accountId: "someAccountId"})
expect(repository.accounts).toEqual([{accountId: "someAccountId"}])
})
В будущих статьях мы углубимся в эти концепции и применим их в реальных приложениях.
Оставайтесь с нами и подписывайтесь на меня на этой платформе и в LinkedIn, где я каждую неделю делюсь идеями о программном обеспечении, дизайне, ООП-практике, открытиях и моих проектах! 💻🏄