Лучшие практики модульного тестирования — Именование

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

Контекст

Итак, давайте предположим, что нас только что завербовал наш друг доктор Оук, который вот-вот закончит свой шедевр, революционный покедекс. К сожалению, разработчики ушли из компании, так и не написав юнит-тестов. Стыд. И, осознавая важность модульного тестирования, он попросил нас сделать это за него. Кажется хорошим, верно?

Именование модульных тестов

Итак, начнем с самого начала. Да, мой дорогой читатель, выбор хорошего имени очень важен для будущего модульного теста. Хотя «TestAdd» и «TestFail» могут показаться приемлемыми вариантами, вы видите, что они не совсем ясны. И почему так? Давайте копаться в этом сейчас. Продолжая модульное тестирование Pokédex, мы собираемся создать тесты для функции «Сканировать». Вы знаете, когда пользователь наводит его на покемона, и он выдает кучу информации. Что-то, что мне действительно понравилось бы в играх.

Первое, что нужно знать об именах, это то, что модульные тесты должны документировать программное обеспечение. Например, давайте проанализируем код ниже:

public PokemonDescription ScanCreature(object creature)
{
    if (creature is not Pokemon pokemon)
        throw new NotAPokemonException("The specified creature is not a Pokémon");

    return pokemon.Description;
}

Кажется достаточно простым. Итак, если бы мы создавали тест для этого класса, мы могли бы подумать, что «ScanWorks» будет понятен, так что давайте! О да, если вам интересно, что это за комментарии, они касаются шаблона AAA, очень аккуратного способа организовать ваши тесты. Вы можете прочитать об этом подробнее здесь.

Идеально верно? Да ну, я не могу утверждать, что это не работает, но есть некоторые проблемы с этим:

  • Не глядя на тестовый код, мы не знаем точно, что значит «работает». Поэтому мы не можем посмотреть на модульные тесты этого класса и сразу узнать, что он должен делать.
  • Что, если метод ScanCreature что-то изменит, например, его возвращаемое значение, и тест сломается? Нам пришлось бы потратить время на поиски того, что должен тестировать код. Конечно, в данном случае это довольно просто, но я видел модульные тесты с более чем 100 строками. Это не красивое зрелище.

Если вы все еще не уверены, позвольте мне немного усложнить ситуацию. Настроим наш метод ScanCreature:

public PokemonDescription ScanCreature(object creature)
{
    if (creature is not Pokemon pokemon)
        throw new NotAPokemonException("The specified creature is not a Pokémon");

    if(pokemon.Generation is null)
        throw new GenerationException("Unable to identify the generation of the Pokémon");

    if(pokemon.Generation != Generation)
        throw new GenerationException("This Pokémon is from a different generation than this pokédex. Consider upgrading to Pokedex PRO.");

    if (pokemon.HasOwner)
        throw new GDPRException("This Pokémon data is protected by the laws of GDPR");

    return pokemon.Description;
}

Итак, теперь, если бы мы перешли к нашему упрощенному подходу к именованию, мы бы использовали что-то вроде «ScanThrows». Но видите ли, у нас есть 3 ситуации, которые могут вызвать исключение, как бы мы их назвали?

Мы могли бы назвать методы тестирования после исключения!

Хотя это не совсем плохая идея, она не сработает, так как у нас есть 2 случая, которые вызовут исключение GenerationException. И следуя идее, что тест должен документировать код, мы не сможем знать, когда должно быть выброшено исключение.

Как, черт возьми, мы их называем?

Это просто: мы просто должны точно сказать, что мы тестируем. Вот хороший шаблон именования, которому я предпочитаю следовать:

MethodName_Scenario_ExpectedResult

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

  • ScanCreature_ShouldReturnPokemonDetails
  • ScanCreature_PokemonHasNoGeneration_ThrowsGenerationException
  • ScanCreature_HasOwner_ThrowsGDPRException
  • ScanCreature_PokemonGenerationIsDifferentThanPokedexGeneration_ThrowsGenerationException

Вы видите, что в первом тестовом примере я не указал сценарий? Это потому, что я лично предпочитаю опускать его, если это нормальное/ожидаемое поведение метода. Но, конечно, мы могли бы придерживаться нашего шаблона и сделать что-то вроде «ScanCreature_NoErrors_ShouldReturnPokemonDetails».

Заключение

Итак, в основном, когда дело доходит до именования, мы хотим сделать его как можно более явным для того, что проверяется модульным тестом. А именно, мы хотим сказать, что тестируется, чего мы ожидаем и когда нам следует этого ожидать.

Похожие записи

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *