Как я могу улучшить свои тесты Junit
Да, мои тесты на джунит выглядят как длинная история:
- Я создаю 4 пользователей
- Я удаляю 1 пользователя
- Я пытаюсь войти в систему с удаленным пользователем и убедиться, что это не удается
- Я вхожу с одним из 3 оставшихся пользователей и подтверждаю, что могу войти
- Я отправляю сообщение от одного пользователя другому и проверяю, что оно появляется в папке "Исходящие" отправителя и в папке "Входящие" получателя.
- Я удаляю сообщение
- ...
- ...
Преимущества: тесты достаточно эффективны (очень хорошо выявляют ошибки) и очень стабильны, потому что они используют только API, если я рефакторинг кода, то тесты тоже рефакторинг. Поскольку я не использую "грязные трюки", такие как сохранение и перезагрузка БД в заданном состоянии, мои тесты не обращают внимания на изменения схемы и реализацию реализации.
Недостатки: тесты усложняются, любое изменение в тесте влияет на другие тесты. Тесты продолжаются 8-9 минут, что отлично подходит для непрерывной интеграции, но немного разочаровывает разработчиков. Тесты нельзя запускать изолированно, лучшее, что вы можете сделать, - это остановиться после того, как интересующий вас тест запустится, но вы обязательно должны запустить все тесты, которые были до этого.
Как бы вы улучшили мои тесты?
9 ответов
Модульные тесты должны - в идеале - быть независимыми и иметь возможность работать в любом порядке. Итак, я хотел бы предложить вам:
- разбить свои тесты, чтобы быть независимым
- рассмотрите возможность использования базы данных в памяти в качестве бэкэнда для ваших тестов
- рассмотреть возможность включения каждого теста или набора в транзакцию, откат которой выполняется в конце
- профилировать юнит-тесты, чтобы увидеть, куда идет время, и сосредоточиться на
если для создания нескольких пользователей и отправки нескольких сообщений требуется 8 минут, проблема с производительностью может отсутствовать в тестах, скорее это может быть признаком проблем с производительностью самой системы - наверняка знает только ваш профилировщик!
[предостережение: я НЕ рассматриваю такие тесты как "интеграционные тесты", хотя я могу быть в меньшинстве; я считаю такие тесты модульными тестами функций, а-ля TDD]
Во-первых, поймите, что тесты, которые у вас есть, являются интеграционными тестами (возможно, для доступа к внешним системам и для широкого круга классов). Модульные тесты должны быть более конкретными, что является проблемой для уже созданной системы. Основная проблема, которая достигается, как правило, заключается в том, как код структурирован:
т.е. класс, тесно связанный с внешними системами (или с другими классами, которые являются). Чтобы сделать это, вам нужно построить классы таким образом, чтобы вы могли избежать попадания во внешние системы во время модульных тестов.
Обновление 1: прочтите следующее и учтите, что получившийся дизайн позволит вам на самом деле проверить логику шифрования, не затрагивая файлы / базы данных - http://www.lostechies.com/blogs/gabrielschenker/archive/2009/01/30/the-dependency-inversion-principle.aspx (не в Java, но очень хорошо иллюстрирует проблему) ... также обратите внимание, что вы можете сделать действительно целенаправленные интеграционные тесты для читателей / писателей вместо того, чтобы тестировать их все вместе.
Я предлагаю:
- Постепенно включайте реальные модульные тесты в вашу систему. Вы можете сделать это, когда вносите изменения и разрабатываете новые функции, соответственно рефакторинг.
- При выполнении предыдущего включайте целевые интеграционные тесты, где это уместно. Убедитесь, что вы можете запускать модульные тесты отдельно от интеграционных тестов.
- Учтите, что ваши тесты близки к тестированию системы в целом, и поэтому отличаются от автоматических приемочных тестов только тем, что работают на границе API. Учитывая это, подумайте о факторах, связанных с важностью API для продукта (например, будет ли он использоваться снаружи), а также о том, имеете ли вы хорошее покрытие с помощью автоматических приемочных тестов. Это может помочь вам понять, какова их ценность в вашей системе, а также почему они, естественно, занимают так много времени. Примите решение о том, будете ли вы тестировать систему в целом на уровне интерфейса или на уровне интерфейса + API.
Обновление 2: Основываясь на других ответах, я хочу кое-что прояснить относительно выполнения TDD. Допустим, вам нужно проверить, отправляет ли какая-то данная логика электронное письмо, регистрирует информацию в файле, сохраняет данные в базе данных и вызывает веб-сервис (не все сразу, я знаю, но вы начинаете добавлять тесты для каждого из них), В каждом тесте вы не хотите воздействовать на внешние системы, что вы действительно хотите проверить, так это то, будет ли логика совершать вызовы к тем системам, которые вы ожидаете. Поэтому, когда вы пишете тест, который проверяет, что электронное письмо отправляется при создании пользователя, вы проверяете, вызывает ли логика зависимость, которая это делает. Обратите внимание, что вы можете написать эти тесты и связанную с ними логику, фактически не выполняя код, отправляющий электронное письмо (а затем обращаясь к внешней системе, чтобы узнать, что было отправлено...). Это поможет вам сосредоточиться на поставленной задаче и поможет вам получить несвязанную систему. Это также упростит проверку того, что отправляется в эти системы.
Теперь вы тестируете много вещей одним методом (нарушение One Assertion Per Test). Это плохо, потому что, когда что-то из этого меняется, весь тест не пройден. Это приводит к тому, что не сразу становится понятно, почему тест не прошел и что нужно исправить. Также, когда вы намеренно изменяете поведение системы, вам нужно изменить больше тестов, чтобы соответствовать измененному поведению (то есть тесты хрупки).
Чтобы узнать, какие тесты хороши, полезно прочитать больше о BDD: http://dannorth.net/introducing-bdd http://techblog.daveastels.com/2005/07/05/a-new-look-at-test-driven-development/ http://jonkruger.com/blog/2008/07/25/why-behavior-driven-development-is-good/
Чтобы улучшить тест, который вы упомянули, я бы разделил его на следующие три класса тестов с этими именами контекста и методов тестирования:
Создание учетных записей пользователей
- Перед созданием пользователя
- Пользователь не существует
- Когда пользователь создан
- пользователь существует
- Когда пользователь удален
- пользователь больше не существует
Вход в систему
- Когда пользователь существует
- пользователь может войти с правильным паролем
- пользователь не может войти с неверным паролем
- Когда пользователь не существует
- пользователь не может войти
Отправка сообщений
- Когда пользователь отправляет сообщение
- сообщение появляется в почтовом ящике отправителя
- сообщение появляется в папке входящих сообщений получателя
- сообщение не появляется ни в каких других окнах сообщений
- Когда сообщение удалено
- сообщение больше не существует
Вам также нужно повысить скорость тестов. У вас должен быть набор модульных тестов с хорошим покрытием, который может быть запущен за пару секунд. Если для запуска тестов требуется больше 10-20 секунд, то вы будете колебаться запускать их после каждого изменения и теряете некоторую быструю обратную связь, которую дает выполнение тестов. (Если он обращается к базе данных, это не модульный тест, а системный или интеграционный тест, который имеет свое применение, но не достаточно быстрый, чтобы выполняться непрерывно.) Вам нужно сломать зависимости тестируемых классов путем насмешки или заглушить их. Также из вашего описания видно, что ваши тесты не являются изолированными, но вместо этого тесты зависят от побочных эффектов, вызванных предыдущими тестами - это нет-нет. Хорошие тесты - ПЕРВЫЕ.
Вы можете использовать JExample, расширение JUnit, которое позволяет методам тестирования иметь возвращаемые значения, которые повторно используются другими тестами. Тесты JExample выполняются с обычным плагином JUnit в Eclipse, а также работают бок о бок с обычными тестами JUnit. Таким образом, миграция не должна быть проблемой. JExample используется следующим образом
@RunWith(JExample.class)
public class MyTest {
@Test
public Object a() {
return new Object();
}
@Test
@Given("#a")
public Object b(Object object) {
// do something with object
return object;
}
@Test
@Given("#b")
public void c(Object object) {
// do some more things with object
}
}
Отказ от ответственности, я среди разработчиков JExample.
Уменьшите зависимости между тестами. Это можно сделать с помощью Mocks. Мартин Фаулер говорит об этом в Mocks - это не заглушки, особенно потому, что mocking уменьшает зависимости между тестами.
Если вы используете TestNG, вы можете комментировать тесты различными способами. Например, вы можете аннотировать свои тесты выше как долгосрочные. Затем вы можете настроить сервер автоматической сборки / непрерывной интеграции для их запуска, но стандартная "интерактивная" сборка разработчика не будет (если они явно не захотят).
Этот подход зависит от разработчиков, регулярно проверяющих вашу непрерывную сборку, чтобы тесты действительно запускались!
Некоторые тесты неизбежно потребуют много времени для запуска. Комментарии в этой теме повторно. производительность все действительны. Однако, если ваши тесты занимают много времени, прагматичным решением будет запустить их, но не позволить их трудоемкому характеру воздействовать на разработчиков до такой степени, что они избегают их запускать.
Примечание: вы можете сделать что-то подобное с JUnit, скажем, присваивая имена тестам в разных модах и заставляя вашу непрерывную сборку запускать определенное подмножество тестовых классов.
Тестируя истории, которые вы описываете, вы получаете очень хрупкие тесты. Если меняется только одна маленькая функциональность, весь ваш тест может быть испорчен. Тогда вы, скорее всего, измените все тесты, на которые влияет это изменение.
На самом деле описываемые вами тесты больше похожи на функциональные или компонентные, чем на модульные. Таким образом, вы используете инфраструктуру модульного тестирования (junit) для не модульных тестов. На мой взгляд, нет ничего плохого в том, чтобы использовать инфраструктуру модульного тестирования для выполнения не модульных тестов, если (и только если) вы знаете об этом.
Так что есть следующие варианты:
Выберите другую инфраструктуру тестирования, которая поддерживает стиль "рассказывания историй" гораздо лучше, как это уже предлагали другие пользователи. Вы должны оценить и найти подходящие рамки тестирования.
Сделайте ваши тесты более "модульными". Поэтому вам нужно будет разбить ваши тесты и, возможно, изменить текущий производственный код. Зачем? Поскольку модульное тестирование направлено на тестирование небольших блоков кода (пуристы модульного тестирования предлагают только один класс одновременно). Благодаря этому ваши юнит-тесты станут более независимыми. Если вы измените поведение одного класса, вам просто нужно изменить сравнительно небольшое количество кода модульного теста. Это делает ваш тестовый модуль более надежным. Во время этого процесса вы можете увидеть, что ваш текущий код не очень хорошо поддерживает модульное тестирование - в основном из-за зависимостей между классами. Это причина того, что вам также нужно будет изменить производственный код.
Если вы находитесь в проекте и у вас заканчивается время, оба варианта могут вам больше не помочь. Тогда вам придется смириться с этими тестами, но вы можете попытаться ослабить свою боль:
Удалите дублирование кода в своих тестах. Как и в рабочем коде, устраните дублирование кода и поместите код в вспомогательные методы или вспомогательные классы. Если что-то меняется, вам может потребоваться только изменить вспомогательный метод или класс. Таким образом, вы сходитесь к следующему предложению.
Добавьте еще один уровень косвенности в свои тесты: создайте вспомогательные методы и вспомогательные классы, которые работают на более высоком уровне абстракции. Они должны выступать в качестве API для ваших тестов. Эти помощники называют вас рабочим кодом. Ваши тесты истории должны вызывать только тех помощников. Если что-то меняется, вам нужно изменить только одно место в вашем API и не нужно трогать все ваши тесты.
Пример подписи для вашего API:
createUserAndDelete(string[] usersForCreation, string[] userForDeletion);
logonWithUser(string user);
sendAndCheckMessageBoxes(string fromUser, string toUser);
Для общего юнит-тестирования я предлагаю взглянуть на тестовые шаблоны XUnit от Gerard Meszaros.
Чтобы преодолеть зависимости в ваших производственных тестах, обратите внимание на статью "Эффективная работа с устаревшим кодом" от Michael Feathers
В дополнение к вышесказанному, возьмите хорошую книгу по TDD (я могу рекомендовать "TDD и Acceptance TDD для разработчиков Java"). Несмотря на то, что он будет подходить с точки зрения TDD, есть много полезной информации о написании правильных видов модульных тестов.
Найдите того, кто обладает достаточными знаниями в этой области, и используйте его, чтобы выяснить, как вы можете улучшить свои тесты.
Присоединяйтесь к списку рассылки, чтобы задавать вопросы и просто читать трафик, проходящий через. Список JUnit на Yahoo (что-то вроде groups.yahoo.com/junit). Некоторые из движителей и шейкеров в мире JUnit находятся в этом списке и принимают активное участие.
Получите список золотых правил юнит-тестов и прикрепите их к вашей (и другим) стенке кабины, что-то вроде:
- Ты никогда не получишь доступ к внешней системе
- Ты должен только тестировать тестируемый код
- Ты должен испытать только одну вещь одновременно и т. Д.
Поскольку все остальные говорят о структуре, я выберу разные моменты. Это звучит как хорошая возможность профилировать код, чтобы найти узкие места и выполнить его через покрытие кода, чтобы увидеть, если вы что-то упустили (учитывая время, необходимое для его выполнения, результаты могут быть интересными).
Я лично использую профилировщик Netbeans, но есть и в других IDE, а также в отдельных.
Для покрытия кода я использую Cobertura, но EMMA тоже работает (у EMMA было раздражение, которого не было у Cobertura... Я забыл, что это было, и это может больше не быть проблемой). Эти двое бесплатны, есть платные и хорошие.