DevGang
Авторизоваться

Тестовые двойники: краткое руководство по повышению навыков тестирования

Добро пожаловать в быстро развивающуюся сферу разработки программного обеспечения, где освоение 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, где я каждую неделю делюсь идеями о программном обеспечении, дизайне, ООП-практике, открытиях и моих проектах! 💻🏄

Источник:

#Testing
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

В этом месте могла бы быть ваша реклама

Разместить рекламу