Раньше я был категорически против этой идеи, но недавно начал передумывать. Вот что случилось.
Плохо
Производственный и тестовый код можно интегрировать на разных уровнях:
- Поддерживается языком.
- Не поддерживается языком, но смешивает производственный и тестовый код в одних и тех же классах.
- Рабочий и тестовый коды находятся в разных классах, но в одном каталоге.
- Производственный и тестовый код живут в разных каталогах.
Я всегда думал, что варианты 2) и 3) — плохая идея, потому что они затрудняют чтение и просмотр кода, они способствуют созданию огромных классов, и они негативно влияют на вашу инфраструктуру сборки (которая теперь должна быть в состоянии убрать) тестовый код, когда вы хотите создать отправляемый двоичный файл). Мы (Google) очень внимательно относимся к этим пунктам, поэтому строго соблюдаем вариант 4) (хотя мы часто помещаем наши тесты в тот же пакет, что и производственный код).
Я думаю, что эта практика является наиболее распространенной, и она работает очень хорошо.
Имея это в виду, разве язык, который изначально поддерживает модульное тестирование, не будет худшим сценарием?
Богоявление
Недавно я размышлял о своих привычках к тестированию за последние десять лет и сделал несколько интересных наблюдений:
- Я чувствую необходимость писать тесты для кода, который я пишу очень часто.
- Точно так же, как часто, эта потребность компенсируется ограничениями окружающей среды, поэтому я не пишу эти тесты.
Мой опыт работы с большим программным обеспечением, большими командами и огромными базами кода. Когда вы работаете в такой среде, компания очень часто разрабатывает собственную тестовую инфраструктуру. Конечно, код остается кодом, но то, как вы его выполняете и как его внедряете, будет сильно отличаться от компании к компании (а иногда даже от проекта к проекту).
Как правило, я кодирую функцию, перебираю ее несколько раз и достигаю точки, когда я довольно доволен ее формой: она выглядит прилично, она выполняет свою работу, и хотя над ней, очевидно, еще предстоит много работы, он достаточно зрел, чтобы писать тесты для него на данный момент не будет пустой тратой.
Код для написания этих тестов обычно довольно очевиден, поэтому я могу довольно быстро сформировать его в своей голове и реализовать его в коде вскоре после этого. Теперь мне нужно найти способ действительно запустить этот тест и сделать его частью нашей большей тестирующей инфраструктуры, и это то, где вещи обычно становятся ужасными. Обычно мне приходится менять или обновлять свою среду, вызывать различные инструменты, извлекать различные вики / HTML-страницы, чтобы освежить в памяти то, что требуется для интеграции моих тестов в общую картину.
Хуже всего то, что мне, вероятно, придется заново учиться всему, когда я переключаюсь на следующий проект или на следующую работу. Опять же, я напишу тест (который довольно прост, поскольку это тот же язык, который я использовал для написания производственного кода), и мне придется изучать совершенно новую среду для запуска этого теста.
Устранить проблему с окружающей средой непросто, но если исходный язык, который я кодирую в поддерживаемых модульных тестах, вероятно, будет гораздо более искушенным написать эти тесты, поскольку 1) теперь существует очевидное место, куда они должны идти, и 2) очень вероятно, что инфраструктура тестирования знает, как выполнить эти тесты, которые я буду писать.
Основным преимуществом здесь является то, что разработчик и инфраструктура тестирования теперь имеют общие знания: разработчик знает, как писать тесты, а инфраструктура знает, как получить доступ к этим тестам. И поскольку этот механизм является частью языка, он останется неизменным независимо от проекта или компании.
Как мы делаем это?
Так как же будет выглядеть язык, который изначально поддерживает модульные тесты?
Я знаю из первых рук, что написание тестового фреймворка непросто, поэтому важно убедиться, что тестовый объект остается разумно ограниченным и не слишком сильно влияет на сложность языка. Вы заметите, что на протяжении всей этой статьи я хотел сказать «модульный тест», а не просто «тест». Поскольку TestNG сосредоточен на обеспечении всего спектра тестирования, я думаю, что для языка важно поддерживать только модульное тестирование, или, точнее, облегчить только тестирование модуля компиляции, в котором находится тест.
Интересно, что очень немногие современные языки поддерживают модульное тестирование, и единственный, который я нашел среди «недавних», — это D (я уверен, что комментаторы будут рады указать мне больше языков).
Подход D довольно минимален: вы можете объявить раздел unittest в своем классе. Это ключевое слово действует как метод, и вы просто пишете свои тесты внутри:
//
// D
//
class Sum
{
int add(int x, int y) { return x + y; }
unittest
{
Sum sum = new Sum;
assert(sum.add(3,4) == 7);
assert(sum.add(-2,0) == -2);
}
}
Это настолько скромно, насколько это возможно. Преимущество состоит в том, что влияние на сам язык минимально, но мне интересно, могу ли я захотеть писать разные методы модульных тестов вместо того, чтобы иметь только один, содержащий все мои тесты. И если мы идем по этому пути, почему бы не сделать ключевое слово unittest эквивалентом класса, а не просто метода?
//
// Pseudo-Java
//
public class Sum {
public int add(int x, int y) { return x + y; }
}
unittest {
public void positiveNumbers() {
Sum sum = new Sum();
assert(sum.add(3,4) == 7);
}
public void negativeNumbers() {
Sum sum = new Sum();
assert(sum.add(-2,0) == -2);
}
}
Как я уже говорил ранее, я думаю, что очень важно, чтобы эта функция оставалась максимально простой, поэтому какие функции из сложных сред тестирования следует удалить, а какие оставить?
Если мы придерживаемся подхода D, возможно, мы мало что сможем добавить, но если мы пойдем к ключевому слову класса, то, вероятно, будут полезны две функции:
- Метод setUp / tearDown (который уже пригодится в приведенном выше примере, где мы создаем новый объект Sum в обоих методах тестирования.
- Исключение тестирования.
На данный момент я уже чувствую себя немного неловко из-за дополнительной сложности, поэтому, возможно, упрощенный подход D — это то, где мы должны провести черту.
Что вы думаете о встроенной поддержке модульного тестирования на языках программирования? А какие функции вы хотели бы увидеть?