Выполнение не фиктивного, основанного на состоянии модульного тестирования нетривиальных функций и их зависимостей, которые следуют CQS - PullRequest
0 голосов
/ 09 февраля 2020

Я понимаю, что этот вопрос может показаться дубликатом таких вопросов, как это , это , это , это , и это . Однако я специально спрашиваю, как бы вы написали модульные тесты, используя стиль Детройта, к нетривиальному коду с несколькими путями кода. Другие вопросы, статьи и объяснения обсуждают тривиальные примеры, такие как класс Calculator. Кроме того, я практикую CQS, или разделение командных запросов, которое меняет методы, с помощью которых я пишу тесты.

Согласно статье Мартина Фаулера " Насмешки не являются заглушками ", я понимаю что к TDD существуют две школы мысли - классическая (Детройт) и мокистская (Лондон).

Когда я впервые изучил модульное тестирование и TDD в целом, меня учили лондонскому стилю, используя Mocking Frameworks, как Java Mockito. Я понятия не имел о существовании Classical TDD.

. Чрезмерное использование Mocks в лондонском стиле беспокоит меня тем, что тесты очень сильно привязаны к реализации, делая их хрупкими. Учитывая, что многие написанные мною тесты носили поведенческий характер с использованием имитаций, я хотел бы узнать и понять, как вы будете писать тесты в классическом стиле.

Для этого у меня есть несколько вопросов. Для классического тестирования:

  1. Должны ли вы использовать реальную реализацию заданной зависимости или фальшивого класса?
  2. Есть ли у практикующих в Детройте иное определение понятия "юнит", чем у Мокистов? do?

Для более подробной информации приведу пример нетривиального реального кода для регистрации пользователя в REST API.

public async signUpUser(userDTO: CreateUserDTO): Promise<void> {
    const validationResult = this.dataValidator.validate(UserValidators.createUser, userDTO);

    if (validationResult.isLeft()) 
        return Promise.reject(CommonErrors.ValidationError.create('User', validationResult.value)); 

    const [usernameTaken, emailTaken] = await Promise.all([
        this.userRepository.existsByUsername(userDTO.username),
        this.userRepository.existsByEmail(userDTO.email)
    ]) as [boolean, boolean];

    if (usernameTaken)
        return Promise.reject(CreateUserErrors.UsernameTakenError.create());

    if (emailTaken)
        return Promise.reject(CreateUserErrors.EmailTakenError.create());

    const hash = await this.authService.hashPassword(userDTO.password);

    const user: User = { id: 'create-an-id', ...userDTO, password: hash };

    await this.userRepository.addUser(user);

    this.emitter.emit('user-signed-up', user);
}

С моим знанием Подход mocking, я бы обычно высмеивал каждую отдельную зависимость здесь, чтобы mocks отвечал с определенными результатами для заданных аргументов, а затем утверждал, что метод хранилища addUser был вызван с правильным пользователем.

Используя классический подход для тестирования у меня будет FakeUserRepository, который работает с коллекцией в памяти и делает утверждения о состоянии репозитория. Проблема в том, что я не уверен, как вписываются dataValidator и authService. Должны ли они быть реальными реализациями, которые фактически проверяют данные и на самом деле имеют sh пароли? Или они тоже должны быть подделками, которые уважают свои соответствующие интерфейсы и возвращают запрограммированные ответы на определенные входные данные?

В других методах Service есть обработчик исключений, который генерирует определенные исключения на основе исключений, выданных из authService. Как вы проводите государственное тестирование в этом случае? Вам нужно создать фейк, который соблюдает интерфейс и генерирует исключения на основе определенных входных данных? Если да, разве мы не вернулись к созданию макетов сейчас?

Чтобы дать вам другой пример функции, для которой я не был бы уверен, как построить фальшивки, посмотрите этот метод декодирования токена JWT, который часть моего AuthenticationService:

public verifyAndDecodeAuthToken(
    candidateToken: string, 
    opts?: ITokenDecodingOptions
): Either<AuthorizationErrors.AuthorizationError, ITokenPayload> {
    try {
        return right(
            this.tokenHandler.verifyAndDecodeToken(candidateToken, 'my-secret', opts) as ITokenPayload
        );
    } catch (e) {
        switch (true) {
            case e instanceof TokenErrors.CouldNotDecodeTokenError:
                throw ApplicationErrors.UnexpectedError.create();
            case e instanceof TokenErrors.TokenExpiredError:
                return left(AuthorizationErrors.AuthorizationError.create());
            default:
                throw ApplicationErrors.UnexpectedError.create();
        }
    }
}

Здесь вы можете видеть, что функция может выдавать разные ошибки, которые будут иметь разные значения для вызывающей стороны API. Если бы я создавал фальшивку здесь, единственное, что я мог подумать, это заставить фальшивку отвечать определенными ошибками на жестко запрограммированные входы, но опять же, сейчас это похоже на пересмотр фреймворка.

* 1049 Так что, в конце концов, я не уверен, как вы пишете модульные тесты без mock-ов, используя классический метод утверждения на основе состояний, и я был бы признателен за любые советы о том, как это сделать для моего примера кода выше. Благодаря.

1 Ответ

1 голос
/ 12 февраля 2020

Если вы используете реальную реализацию заданной зависимости или фальшивого класса?

Как показывает ваш собственный опыт, чрезмерное использование mock делает тесты хрупкими. Следовательно, вы должны использовать макеты (или другие виды тестовых двойников) только в том случае, если есть причина для этого. Наилучшие причины для использования двойников теста:

  • Вы не можете легко заставить зависимый компонент (DO C) вести себя так, как задумано для ваших тестов. Например, ваш код является надежным и проверяет, не указывает ли состояние возврата другого компонента на сбой. Чтобы проверить код надежности, вам нужен другой компонент, чтобы вернуть статус ошибки, но это может быть ужасно трудно или даже невозможно с реальным компонентом.
  • Вызывает ли DO C какой-либо derministi c поведение (дата / время, случайность, сетевые подключения)? Например, если вычисления вашего кода используют текущее время, то с реальным DO C (то есть модулем времени) вы получите разные результаты для каждого запуска теста.
  • Будет ли результат, который Вы хотите проверить некоторые данные, которые тестируемый код передает в DO C, но DO C не имеет API для получения этих данных? Например, если тестируемый код записывает свой результат в консоль (в данном случае консоль - DO C), но у ваших тестов нет возможности запросить консоль о том, что было в нее записано.
  • Тестовая настройка для реального DO C слишком сложна и / или требует интенсивного обслуживания (например, необходимость во внешних файлах). Например, DO C анализирует некоторые файлы конфигурации по фиксированному пути. Кроме того, для разных тестовых случаев вам потребуется по-разному конфигурировать DO C, и, следовательно, вам придется предоставить другой файл конфигурации в этом месте.
  • Исходный DO C приносит проблемы с переносимостью для вашего теста. код. Например, если ваша функция hashPassword использует некоторое оборудование cryptographi c для вычисления ha sh, но это оборудование (или правильная версия оборудования) доступно не на всех хостах, где выполняются юнит-тесты.
  • Вызывает ли использование оригинального DO C неприемлемо длительное время сборки / выполнения?
  • Имеет ли DO C стабильность (зрелость) проблемы, которые делают тесты ненадежными, или, что еще хуже, DO C даже еще не доступен?
  • Может быть, сам DO C не имеет никаких вышеупомянутых проблем, но имеет собственные зависимости, и результирующий набор зависимостей приводит к некоторым из упомянутых проблем выше?

Например, вы (обычно) не высмеиваете стандартные библиотечные математические функции, такие как sin или cos, потому что у них нет ни одной из вышеупомянутых проблем.

...