Это легко писать «модульные тесты» , что использование JUnit и некоторые насмешливые библиотеки. Они могут создавать покрытие кода, которое удовлетворяет некоторые заинтересованные стороны, даже если тесты могут даже не быть модульными тестами, и тест может дать сомнительную ценность. Также может быть очень легко написать модульные тесты, которые (в теории) являются модульными, но гораздо сложнее, чем базовый код, и, следовательно, просто добавляют к общей энтропии программного обеспечения .
Этот конкретный тип программного обеспечения энтропии имеет неприятную характеристику , что делает его еще труднее для базового программного обеспечения , чтобы быть изменено или на поверхность новых требований. Это как тест имеет отрицательное значение .
Правильно выполнить юнит-тестирование намного сложнее, чем думают люди. В этой статье я обрисую несколько советов, направленных на улучшение читабельности, удобства обслуживания и качества ваших модульных тестов.
Примечание: для фрагментов кода используется Спок . Для тех, кто не знает Спока, рассмотрите это очень мощный DSL вокруг JUnit, который добавляет некоторые приятные функции и сокращает многословность.
Причина неудачи
Модульный тест должен провалиться, только если есть проблема с тестируемым кодом . Модульный тест для класса DBService должен завершиться неудачей, только если есть ошибка с DBService, а не если есть ошибка с любым другим классом, от которого он зависит. Таким образом, в модульном тесте для DBService единственным экземпляром объекта должен быть DBService. Любой другой объект, от которого зависит DBService, должен быть заглушен или смоделирован .
В противном случае вы тестируете код за пределами DBService. В то время как вы можете (неправильно) думать, что это больше денег , поиск основной причины проблем займет больше времени. Если тест не пройден, это может быть связано с тем, что существует проблема с любым из нескольких классов, но вы не знаете, какой именно. Принимая во внимание, что, если это может произойти сбой только потому, что тестируемый код неверен, то вы знаете, что вы окажетесь ближе к месту возникновения проблемы гораздо быстрее.
Более того, такое мышление улучшит объектно-ориентированную природу вашего кода. Если тест не пройден только потому, что тестируемый код нарушен, это означает, что тест только проверяет обязанности класса и, таким образом, проясняет их. Если обязанности класса не ясны, или он не может ничего сделать без другого класса, или класс настолько тривиален, тест не имеет смысла, возникает вопрос, что с классом что-то не так с точки зрения общности его обязанностей ,
Единственное исключение не издевается или гася зависимый класс, если вы используете хорошо известный класс из библиотеки Java , например , String. В этом нет особого смысла заглушки или насмешки. Или класс зависимостей настолько прост, например, неизменяемый POJO, что не имеет большого значения, чтобы заглушки или насмешки над ним.
Заглушка и издевательство
Термины « издевательство» и « заглушка» часто можно использовать взаимозаменяемо, как если бы они были одним и тем же. Они не одно и то же . Так в чем же разница? Таким образом, если ваш тестируемый код имеет зависимость от объекта, для которого он никогда не вызывает метод для этого объекта, который имеет побочные эффекты , этот объект должен быть заглушен.
Принимая во внимание, что если у него есть зависимость от объекта, для которого он вызывает методы, которые имеют побочные эффекты, то это должно быть посмешищем. Почему это важно? Потому что ваш тест должен проверять разные вещи в зависимости от типов отношений, которые он имеет с его зависимостями.
Допустим, ваш тестируемый объект — BusinessDelegate . BusinessDelegate получает запросы на редактирование BusinessEntities . Он выполняет простую бизнес-логику, а затем вызывает методы в DBFacade (класс фасада перед базой данных). Итак, тестируемый код выглядит так:
Джава
x
1
public class BusinessDelegate {
2
private DBFacade dbFacade;
3
// ...
4
5
public void edit(BusinessEntity businessEntity) {
6
// Read some attributes on the business entity
7
String newValue = businessEntity.getValue();
8
9
// Some Business Logic, Data Mapping, and / or Validation
10
//...
12
dbFacade.update(index, data)
13
}
14
}
Что касается класса BusinessDelegate, мы можем видеть две взаимосвязи.
- Отношения только для чтения с BusinessEntity. BusinessDelegate вызывает несколько методов getters () и никогда не меняет своего состояния или не вызывает никаких методов, которые имеют побочные эффекты.
- Отношения с DBFacade, где он просит DBFacade сделать что-то, что, как мы предполагаем, будет иметь побочные эффекты. BusinessDelegate не несет ответственности за обеспечение обновления, что является задачей DBFacade. BusinessDelegate отвечает за то, чтобы метод обновления вызывался только с правильными параметрами.
Таким образом, модульный тест для BusinessDelegate, BusinessEntity должен быть заглушен, а DBFacade должен быть смоделирован. Если бы мы использовали среду тестирования Спока, мы могли бы видеть это
Groovy
xxxxxxxxxx
1
class BusinessDelegateSpec {
2
3
BusinessDelegate businessDelegate
4
def dbFacade
5
6
def setup() {
7
dbFacade = Mock(DbFacade)
8
businessDelegate = new BusinessDelegate(dbFacade);
9
}
10
11
def "edit(BusinessEntity businessEntity)"() {
12
given:
13
def businessEntity = Stub(BusinessEntity)
14
// ...
15
when:
16
businessDelegate.edit(businessEntity)
17
then:
18
1 * dbFacade.update(data)
19
}
20
}
Имея хорошее представление о заглушке / издеваться дифференциация повышает качество OO резко . Вместо того, чтобы просто думать о том, что делает объект, отношения и зависимости между ними становятся гораздо более сфокусированными. Теперь модульные тесты могут помочь реализовать принципы проектирования, которые в противном случае просто потерялись бы.
Заглушка и издеваться в нужном месте
Любопытным из вас может быть вопрос, почему в приведенном выше примере кода dbFacade был объявлен на уровне класса , а businessEntity был объявлен на уровне метода ? Ну, ответ таков: код модульного теста гораздо удобнее для чтения, когда он отражает проверяемый код . В реальном классе BusinessDelegate зависимость от dbFacade находится на уровне класса, а зависимость от BusinessEntity - на уровне метода.
В реальном мире, когда создается экземпляр BusinessDelegate, существует зависимость DBFacade, и каждый раз, когда создается экземпляр BusinessDelegate для модульного теста, поэтому вполне допустимо иметь также зависимость DBFacade.
Звучит разумно? Надеюсь на это. Есть еще два преимущества:
- Уменьшение детализации кода. Даже используя Спок, юнит-тесты могут стать многословными. Если вы удалите зависимости уровня класса из модульного теста , вы уменьшите детализацию тестового кода. Если у вашего класса есть зависимость от четырех других классов на уровне класса, то минимум четыре строки кода из каждого теста.
- Согласованность. Разработчики, как правило, пишут юнит-тесты по-разному. Хорошо, если они единственные, кто читает свой код; но это редко так. Следовательно, чем больше у нас согласованности между тестами, тем легче их поддерживать. Поэтому, если вы прочитаете тест, который вы никогда не читали раньше, и по крайней мере увидите, что переменные заглушены и смоделированы в определенных местах по определенным причинам, вам будет проще читать код модульного теста.
Порядок декларации переменных
Это продолжение из последнего пункта. Объявление переменных в правильном месте - отличное начало, следующее - сделать в том же порядке, в каком они появляются в коде. Итак, если у нас есть что-то вроде ниже.
Джава
x
1
public class BusinessDelegate {
2
private BusinessEntityValidator businessEntityValidator;
3
private DbFacade dbFacade;
4
private ExcepctionHandler exceptionHandler;
5
6
7
BusinessDelegate(BusinessEntityValidator businessEntityValidator, DbFacade dbFacade, ExcepctionHandler exceptionHandler) {
8
// ...
9
// ...
10
}
11
12
public BusinessEntity read(Request request, Key key) {
13
// ...
14
}
15
16
}
Считать тестовый код намного проще, если их заглушки и макеты определены в том же порядке, в котором они отображаются в тестируемом классе.
Groovy
x
1
class BusinessDelegateSpec {
2
BusinessDelegate businessDelegate
3
4
// class level dependencies in the same order
5
def businessEntityValidator
6
def dbFacade
7
def exceptionHandler
8
9
def setup() {
10
businessEntityValidator = Stub(BusinessEntityValidator)
11
dbFacade = Mock(DbFacade)
12
exceptionHandler = Mock(ExceptionHandler)
13
14
businessDelegate = new BusinessDelegate(businessEntityValidator, dbFacade, exceptionHandler)
15
}
16
17
18
19
def "read(Request request, Key key)"() {
20
given:
21
def request = Stub(Request)
22
def key = Stub(key)
23
when:
24
businessDelegate.read(request, key)
25
then:
26
// ...
27
}
28
29
}
Именование переменных
И если вы думали, что последний пункт был педантичным, вы будете рады узнать, что этот пункт тоже. Имена переменных, используемые для представления заглушек и макетов, должны совпадать с именами , которые используются в реальном коде.
Еще лучше, если вы можете назвать переменную так же, как тип в тестируемом коде и не потерять никакого делового значения, тогда сделайте это. В последнем примере кода переменные параметров называются requestInfo и key, а соответствующие заглушки имеют одинаковые имена. Это намного проще, чем делать что-то вроде этого:
Джава
1
//..
2
public void read(Request info, Key someKey) {
3
// ...
4
}
5
// corresponding test code
7
def "read(Request request, Key key)"() {
8
given:
9
def aRequest = Stub(Request)
10
def myKey = Stub(key) // you ill get dizzy soon!
11
12
// ...
Избегать чрезмерного
Слишком много глушения (или насмешек) обычно означает, что что-то пошло не так. Давайте рассмотрим закон Деметры . Представьте себе какой-нибудь телескопический вызов метода ...
xxxxxxxxxx
1
List<BusinessEntity> queryBusinessEntities(Request request, Params params) {
3
// check params are allowed
4
Params paramsToUpdate = queryService.getParamResolver().getParamMapper().getParamComparator().compareParams(params)
5
// ...
6
// ...
7
}
Недостаточно заглушить queryService . Теперь все, что возвращается getParamResolver (), должно быть заглушено, и эта заглушка должна иметь заглушку getParamMapper (), которая затем должна иметь заглушку getParamComoparator () и затем сравнивать заглушку CompareParams (). Даже с таким хорошим фреймворком, как Spock, который сводит к минимуму многословность, у вас будет несколько строк тестового кода, просто заглушки для того, что является только одной строкой кода Java, которая тестируется !!!
Groovy
x
1
def "queryBusinessEntities()"() {
2
given:
3
def params = Stub(Params)
4
def paramResolver = Stub(ParamResolver)
5
queryService.getParamResolver() = paramResolver
6
7
def paramMapper = Stub(ParamMapper)
8
paramResolver.getParamMapper() >> paramMapper
9
10
def paramComparator = Stub (ParamComparator)
11
paramMapper.getParamComparator() >> paramComparator
12
13
Params paramsToUpdate = Stub(Params)
14
15
paramComparator.comparaParams(params) >> paramsToUpdate
16
17
when:
18
// ...
19
then:
20
// ...
21
}
Тьфу! Посмотрите, что эта строка Java делает с нашим модульным тестом. Это становится еще хуже, если вы не используете что-то вроде Спока. Решение состоит в том, чтобы избежать вызова телескопического метода и попытаться просто использовать прямые зависимости.
В этом случае просто вставьте paramComparator непосредственно в наш класс. Тогда код становится ...
Джава
x
1
List<BusinessEntity> queryBusinessEntities(Request request, Params params) {
2
// check params are allowed
3
Params paramsToUpdate = paramComparator.compareParams(params)
4
// ...
5
// ...
6
}
и тестовый код становится
Groovy
x
1
setup() {
2
// ...
3
// ...
4
paramComparator = Stub (ParamComparator)
5
businessEntityDelegate = BusinessEntityDelegate(paramComparator)
6
}
7
def "queryBusinessEntities()"() {
9
given:
10
def params = Stub(Params)
11
Params paramsToUpdate = Stub(Params)
12
paramComparator.comparaParams(params) >> paramsToUpdate
13
when:
15
// ...
16
then:
17
// ...
18
}
Внезапно люди должны благодарить вас за то, что вы чувствуете головокружение.
Огурец Синтаксис
У плохих юнит-тестов повсюду ужасные вещи вроде утверждений. Это может очень быстро вызвать тошноту.
Схематичным вещам всегда легче следовать . Это реальное преимущество синтаксиса Gherkin . Сценарий настроен в данном : всегда. , Когда сценарий и затем где мы проверяем , что мы ожидаем.
Использование чего-то вроде Спока означает, что у вас есть хороший, аккуратный DSL, так что данный::, когда:, и затем: все могут быть размещены в одном тестовом методе.
Узкий Когда Широкий Тогда
Если модульный тест тестирует четыре метода, это модульный тест? Рассмотрим следующий тест:
Джава
1
def "test several methods" {
2
given:
3
// ...
4
when:
5
def name = personService.getname();
6
def dateOfBirth = personService.getDateOfBirth();
7
def country = personService.getCountry();
8
then:
9
name == "tony"
10
dateOfBirth == "1970-04-04"
11
country == "Ireland"
12
}
Во-первых, если Дженкинс скажет вам, что это не удалось, вам придется поболтать и выяснить, какая часть класса неправильна. Поскольку тест не фокусируется на конкретном методе, вы сразу не знаете, какой метод дает сбой. Во-вторых, если getName () не работает, как вы узнаете, работают ли getDateOfBirth () и getCountry ()?
Тест останавливается при первом сбое. Поэтому, когда тест не пройден, вы даже не знаете, работает ли один метод или три метода не работают. Вы можете говорить всем, что у вас есть 99% покрытия кода и один тест не пройден, но не ясно, что вы пропустили!
Кроме того, что обычно легче исправить? Маленький тест не пройден или длинный тест не пройден? Никаких призов за получение этого правильного!
Тест должен проверять отдельное взаимодействие с классом, который вы тестируете. Теперь, это не означает, что вы можете иметь только одно утверждение, это означает, что у вас должен быть узкий, когда и широкий тогда.
Итак, давайте начнем с узкого . В идеале, только одна строка кода . Одна строка кода соответствует методу, который вы тестируете.
Groovy
xxxxxxxxxx
1
def "getName()" {
2
given:
3
// ...
4
when:
5
def name = personService.getname();
6
then:
7
name == "tony"
8
}
9
def "getDateOfBirth()" {
11
given:
12
// ...
13
when:
14
def dateOfBirth = personService.getDateOfBirth();
15
then:
16
dateOfBirth == "1970-04-04"
17
}
18
def "getCountry()" {
20
given:
21
// ...
22
when:
23
def country = personService.getCountry();
24
then:
25
country == "Ireland"
26
}
27
Теперь мы могли бы иметь точно такое же покрытие кода, если getName () завершится неудачно, но getCountry () и getDateOfBirth () пройдут. Но есть проблема с getName (), а не с getCountry () и getDateOfBirth () . Правильное получение гранулярности теста важно. В идеале это должен быть минимум один модульный тест для каждого не частного метода и более, если вы учитываете отрицательные тесты и т. Д.
Теперь вполне нормально иметь несколько утверждений в модульном тесте. Например, предположим, что у нас есть метод, который делегирован другим классам.
Рассмотрим метод resynceCache (), который в своей реализации вызывает два других метода для объекта cacheService, clear () и reload ().
xxxxxxxxxx
1
def "resyncCache()" {
2
given:
3
// ...
4
when:
5
personService.resyncCache();
6
then:
7
1 * cacheService.clear()
8
1 * cacheService.reload()
9
}
В этом случае было бы не имеет смысла иметь два отдельных тестовых сек . « Когда» то же самое, и если любой из них терпит неудачу, вы сразу же знаете, какой метод вы должны посмотреть. Проведение двух отдельных тестов означает вдвое больше усилий и мало пользы. Тонкая вещь, чтобы получить прямо здесь, это убедиться, что ваши утверждения в правильном порядке . Они должны быть в том же порядке, что и при выполнении кода .
Итак, clear () вызывается перед reload () . Если проверка завершается неудачно при clear (), нет особого смысла проверять reload (), так как метод не работает. Если вы не следуете совету по порядку утверждений и сначала утверждаете при помощи reload (), и это считается ошибочным, вы не узнаете, произошла ли clear (), которая должна произойти в первую очередь. Мышление таким образом поможет вам стать тестовым ниндзя!
Порядок подсказок для насмешек и окурков, то же самое относится и к утверждению. Утверждать в хронологическом порядке. Это педантично, но это сделает тестовый код гораздо более понятным.
Параметризация
Параметризация является очень мощной возможностью, которая может значительно уменьшить детализацию тестового кода и быстро увеличить охват ветвлений в путях кода. Юнит ниндзя должен всегда уметь определять, когда его использовать!
Очевидным признаком того, что ряд тестов может быть сгруппирован в один тест и параметризован, является то, что они имеют одинаковые блоки, за исключением разных входных параметров.
Например, рассмотрим ниже.
Groovy
xxxxxxxxxx
1
def "addNumbers(), even numbers"() {
2
given:
3
// ...
4
when:
5
def answer = mathService.addNumbers(4, 4);
6
then:
7
// ...
8
}
9
def "addNumbers(), odd numbers"() {
11
given:
12
// ...
13
when:
14
def answer = mathService.addNumbers(5, 5);
15
then:
16
// ...
17
}
Как мы видим здесь, когда то же самое, кроме входных параметров. Это не просто для параметризации.
Groovy
xxxxxxxxxx
1
"number1=#number1, number2=#number2") // unroll will provide the exact values in test report (
2
def "addNumbers()"(int number1, int number2) {
3
given:
4
// ...
5
when:
6
def answer = mathService.addNumbers(number1, number2);
7
then:
8
// ...
9
where:
10
number1 | number2 || answer
11
4 | 4 || 8
12
5 | 5 || 10
13
}
Сразу же мы получаем сокращение кода на 50%. Мы также упростили добавление дополнительных перестановок, просто добавив еще одну строку в таблицу where.
Таким образом, хотя может показаться очень очевидным, что эти два теста должны были быть одним параметризованным тестом, это очевидно только в том случае, если придерживаться максимума наличия узкого интервала. Узкий « когда» кодирования стиль делает точный сценарий тестируется намного легче увидеть. Если широко используется когда много вещей происходит, это не так, и поэтому выбор тестов для параметризации сложнее.
Как правило, единственное время, чтобы не параметризовать тест с таким же синтаксическим выражением где: кодовый блок, - это когда ожидания имеют совершенно разные структуры. Например, ожидание исключения в одном сценарии, а int - это другое средство для двух разных структур. В таких сценариях лучше не параметризировать. Примером этого анти-паттерна является смешивание положительного и отрицательного теста в одном тесте, вы никогда не должны делать это.
Предположим, что наш метод addNumbers () сгенерирует исключение, если он получит число с плавающей запятой, это отрицательный тест и должен храниться отдельно. Когда разработчики задают параметры, когда им не нужно, в тесте появляется запах кода оператора if .
Когда: блок кода или то: код блок никогда не должен содержать , если заявление . Это признак того, что тестовые перестановки имеют слишком много логических различий и должны быть отдельными тестами.
Резюме
Чистое модульное тестирование является неотъемлемой частью достижения поддерживаемой базы кода. Это имеет первостепенное значение, если вы хотите иметь возможность выпускать регулярно и быстро. Это также поможет вам получить больше удовольствия от разработки программного обеспечения!