Статьи

Как написать эффективные юнит-тесты

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

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

Чего не делать

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

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

Некоторые распространенные анти-паттерны:

  • Модульные тесты для одной единицы кода позволяют этому коду вызывать все его зависимости
  • Модульные тесты загружают большую базу данных перед началом
  • Модульные тесты медленные, поэтому вы ненавидите добавлять и запускать их
  • Утверждение для многих вещей в одном тесте или утверждение для всего вложенного объекта JSON, что является одним и тем же

    • Подумайте, что произойдет, если вы когда-нибудь измените эту схему JSON
  • Забудьте тестировать ветки в логике вашего кода
  • Тесты многословны

Что делать вместо

  • Макет других частей кода базы
  • Ложный доступ к базе данных, когда вы можете

    • или убедитесь, что доступ к базе данных в ваших тестах быстрый, чтобы настроить и сбросить между тестами
    • Вы можете использовать транзакции базы данных для этого
  • Разработать механизм для запуска только одного теста или небольшого набора тестов по требованию.
  • Каждая логическая ветвь в вашем коде проходит свой собственный модульный тест
  • Тесты используют те же принципы DRY, что и любой другой код

Пример: приложение Flask

Вот пример приложения Flask с вымышленным слоем ORM.

from flask import Flask
from flask.json import jsonify

app = Flask(__name__)

@app.route('/user/<int:user_id'>/)
def get_user(user_id):
    if not request.user:
        return jsonify({'error': 'You are not logged in'})
    try:
        user = User.objects.get(id=user_id)
    except NotFoundError as e:
        return jsonify({'error': e})
    return jsonify(dict(id=user.id, name=user.name, email=user.email))

Какие тесты вы хотите написать для этого кода и как они будут изолированы от остальной части вашего кода?

  • Проверьте, что если вы сделаете, GET /user/1то ваше представление вызывается с user_id1.

    • Вам не нужно тестировать, @app.routeскажем так, но вам нужно проверить, работает ли настроенная вами часть (URL).
    • Проверьте, что происходит, когда вы вызываете этот маршрут с нецелым числом.
    • Просто убедитесь, что get_userвызывается с правильным параметром, НЕ выполняйте тело в этом тесте. Если вы это сделаете, поломка там провалит это испытание, а также последующие испытания.
    • Например, что если вы ошиблись /userили забыли указать, что это user_idбыл int.
  • Проверьте, что возвращается, если request.userНЕ определено. Это ветка в вашем коде.

    • Если вы превратите это в декоратор, вы просто захотите убедиться, что декоратор выполняется, но вы можете отложить тестирование логики до тестов для этого компонента. Это пример улучшения дизайна вашего компонента с помощью тестирования.
  • Проверьте, что происходит, если user_idобъект не найден.
  • Проверьте возвращаемое значение успешного вызова.

    • Опять же, это может быть сделано лучше, если иметь User.to_json()метод, который тестируется отдельно.
    • В этом случае вы просто заявите, что возвращаемое значение равно User.to_json(), а не фактическому JSON.

Для некоторых из этих тестовых случаев вам user_idсначала потребуется создать запись в базе данных для этого . Ваша среда тестирования должна предоставить вам удобное место для этого. Опять же, лучший способ сделать это — написать код для создания именно той записи, которая вам нужна для этого набора тестов, вместо того, чтобы выполнять тесты с полной резервной копией базы данных. Последнее может быть довольно трудно поддерживать, и, как правило, медленнее.

В настоящее время я работаю в NerdWallet , стартапе в Сан-Франциско, пытаясь внести ясность во все финансовые решения жизни. Мы нанимаем как сумасшедшие . Напишите мне в твиттере, я бы с удовольствием пообщался