За свою карьеру в программном обеспечении я столкнулся с широким спектром взглядов и мнений по поводу тестирования кода. Две крайности заключаются в том, что «тесты не стоит писать, потому что что-то слишком сложно» или что «каждый проверяемый фрагмент кода должен сопровождаться тестами». Из этих двух противоположных мнений последнее, хотя и не всегда в такой крайней форме, гораздо более распространено. Здесь я приведу три случая, почему нам не всегда нужно тестировать код: очевидная корректность может быть у изолированных фрагментов кода; избыточность плохо связанных тестов может возникнуть при рефакторинге; и часто неизменяемость критического для бизнеса кода. Вместо этого я считаю, что мы должны тщательно продумать, где действительно необходимы тесты, прежде чем внедрять их.

Очевидное

Если вы когда-либо проходили обучение, смотрели курс или читали книгу по модульному тестированию, вы, вероятно, видели пример, который тестирует фрагмент кода следующим образом:

func Sum(x int, y int) int {
    return x + y;
}

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

Однако все эти учебники не учитывают, требует ли функция теста в первую очередь. Глядя на приведенный выше пример, думаете ли вы, что он не делает то, за что претендует? Можно ли выразить это проще? Тяжело уложить голову? Ответ на все три вопроса (надеюсь) нет. Это иллюстрирует, как код может быть интуитивно правильным с первого взгляда, без необходимости тщательного доказательства или тестирования. Сэр Тони Хоар, чрезвычайно влиятельный ученый-компьютерщик, позорно сказал следующее:

"Есть два способа создания части программного обеспечения: один – сделать его настолько простым, чтобы в нем явно не было ошибок, а второй – сделать его настолько сложным, чтобы в нем не было очевидных ошибок".

Этот кусок риторики идеально подходит к вопросам, которые мы задавали в примере Sum. На практике мы видим, что тесты действительно нужны только тогда, когда что-то «настолько сложно, что нет явных ошибок». Затем эти тесты докажут ценность, показав, что этих неочевидных ошибок не существует. Итак, для простого, «очевидно» правильного кода нужно ли добавлять тесты? Вместо этого, прежде чем добавлять тесты, вы должны задать вопрос: «Является ли этот код очевидно правильным, или я могу изменить его, чтобы он был очевидно правильным?». Если ответ на этот вопрос положительный, то нет необходимости проверять то, что очевидно.

Пара

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

Эта система проста и изначально имеет полный смысл. Это также общепринятая практика. Тем не менее, он не признает, что одноразовость кода или возможность рефакторинга могут быть важным фактором при выборе тестов и способов их написания. В любой системе, работающей в непрерывном режиме, единицы или отдельные фрагменты кода появляются, исчезают и со временем принимают совершенно разные формы. Это естественный прогресс и эволюция работающего, живого программного обеспечения. Чтобы подчеркнуть этот момент, я спрашиваю: «Вы когда-нибудь рефакторили раздел кодовой базы, чтобы обнаружить, что существующие модульные тесты стали совершенно неуместными или излишними?». Если это так, это показывает, что первоначальные тесты были чрезмерно связаны с макетом и структурой кода. Помните, что тесты — это просто еще один код, который согласуется с исходным кодом, который вы только что написали (или, если выполняется TDD, это просто еще один код, который согласуется с кодом, который вы собираетесь написать).

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

Это, однако, ставит интересную загадку: как мы узнаем, когда код может измениться в структуре или подходе в будущем? Если бы мы могли определить эти области заранее, то наше новообретенное предвидение могло бы просто означать, что мы записываем их в лучшей форме с первого раза. К сожалению, однако, мы остаемся в неведении: попытки организовать код — это подход «все усилия», учитывая текущий уровень знаний.

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

В целом, возраст, стабильность и неопределенность системы должны лежать в основе того, какие тесты мы пишем: пирамида тестирования дает упрощенное представление о мире, но это полезный инструмент для рассмотрения. Однако нам нужно дополнить это нашим пониманием кода и его эволюции с течением времени, задав вопрос: «Как долго эти тесты будут актуальны?» или «Вероятно ли, что они будут неактуальны через X месяцев/лет?».

Неподвижный

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

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

Позже я понял, что причина невероятно проста. Исходный код был написан как доказательство концепции. Это сработало и в результате стало производственным кодом. Никто не хотел вносить какие-либо изменения из страха вызвать неизвестную регрессию, которую было бы невероятно сложно и дорого отследить и исправить. Точно так же процесс назначения цены был фиксированной частью логики: он не менялся со временем, никакие новые требования не меняли то, как он работал, и никому не нужно было знать, как он работает внутри — просто он это делал. Стоимость отсутствия тестов, даже для такого важного фрагмента кода, значительно перевешивалась риском изменения кода, чтобы сделать его тестируемым, и усилиями по его тестированию.

Выступаю ли я за то, чтобы здесь не тестировать важные бизнес-системы? Нет — совсем нет! Однако важно понимать, что мы живем не в идеальном мире. Системы, в которых отсутствуют тесты для важных частей, существуют везде, и их гораздо больше, чем мне хотелось бы признать. Однако это не та катастрофа, о которой я думал в молодости. Если кусок кода сложен, но работает и никогда не меняется, то имеет ли значение, что он плохо протестирован? Тем не менее, добавление тестов при внесении изменений все же было бы благоразумно, но мы все же можем задать вопрос: «Перевешивает ли польза от тестирования этого фрагмента кода сложность добавления тестов?». Это опасный вопрос, и ответ почти всегда «да — добавьте тесты». Но, может быть, иногда стоит задуматься об этом.

Заключить

Подход к созданию хорошо спроектированных наборов тестов, обеспечивающих постоянную ценность на протяжении всего жизненного цикла проекта, является сложной задачей. Сторонники подхода «пирамиды тестирования» чрезмерно упрощают дело. Хотя намерение хорошее, оно не может укорениться в практичности постоянно меняющегося мира разработки программного обеспечения: эволюция кода с течением времени может легко сделать тесты излишними или ненужными, а иногда эти тесты могут даже стать препятствием для рефакторинга. . «Очевидный» характер чистого кода также снижает потребность в тестах как доказательстве правильного поведения. Точно так же простой анализ затрат и выгод следует учитывать при рассмотрении существующего кода, который, как известно, является правильным и неизменным или изменяется очень редко. Не все тесты стоит писать. Не все нужно тестировать, и это нормально.