Концепции автоматического тестирования
- понедельник, 19 марта 2018 г. в 09:07:03
Здравствуйте, меня зовут Дмитрий Карловский и у меня, к сожалению, нет времени писать большую статью, но очень хочется поделиться некоторыми идеями. Поэтому позвольте потестировать на вас небольшую заметку о программировании. Речь сегодня пойдёт об автоматическом тестировании:
От более важного к менее:
На всякий случай подчеркну, что речь идёт исключительно про автоматическое тестирование.
Чем больше тестов, тем медленнее идёт разработка.
Чем полнее тесты, тем быстрее идёт рефакторинг и тестирование, и как следствие поставка новой функциональности.
В зависимости от приоритетов, можно выделить несколько основных стратегий:
Чтобы моя аналитика не была совсем уж голословной, давайте создадим простейшее приложение из двух компонент. Оно будет содержать поле ввода имени и блок с выводом приветствия, адресованного этому имени.
$my_hello $mol_list
rows /
<= Input $mol_string
value?val <=> name?val \
<= Output $my_hello_message
target <= name -
$my_hello_message $mol_view
sub /
\Hello,
<= target \
Тем, кто не знаком с этой нотацией, предлагаю взглянуть на эквивалентный TypeScript код:
export class $my_hello extends $mol_list {
rows() {
return [ this.Input() , this.Output() ]
}
@mem
Input() {
return this.$.$mol_string.make({
value : next => this.name( next ) ,
})
}
@mem
Output() {
return this.$.$my_hello_message.make({
target : ()=> this.name() ,
})
}
@mem
name( next = '' ) { return next }
}
export class $my_hello_message extends $mol_view {
sub() {
return [ 'Hello, ' , this.target() ]
}
target() {
return ''
}
}
@mem
— реактивный кэширующий декоратор. this.$
— di-контекст. Связывание происходит через переопределение свойств. .make
просто создаёт экземпляр и переопределяет указанные свойства.
При этом подходе мы используем реальные зависимости всегда, когда это возможно.
Что следует мокать в любом случае:
Итак, сперва пишем тест на вложенный компонент:
// Components tests of $my_hello_message
$mol_test({
'print greeting to defined target'() {
const app = new $my_hello_message
app.target = ()=> 'Jin'
$mol_assert_equal( app.sub().join( '' ) , 'Hello, Jin' )
} ,
})
А теперь добавляем тесты на внешний компонент:
// Components tests of $my_hello
$mol_test({
'contains Input and Output'() {
const app = new $my_hello
$mol_assert_like( app.sub() , [
app.Input() ,
app.Output() ,
] )
} ,
'print greeting with name from input'() {
const app = new $my_hello
$mol_assert_equal( app.Output().sub().join( '' ) , 'Hello, ' )
app.Input().value( 'Jin' )
$mol_assert_equal( app.Output().sub().join( '' ), 'Hello, Jin' )
} ,
})
Как можно заметить, всё, что нам потребовалось — это публичный интерфейс компонент. Обратите внимание, нам всё равно через какое свойство и как передаётся значение в Output. Мы проверяем именно требования: чтобы выводимое приветствие соответствовало введённому пользователем имени.
Для модульных тестов необходимо изолировать модуль от остального кода. Когда модуль никак не взаимодействует с другими модулями, тесты получаются такими же, как и компонентные:
// Unit tests of $my_hello_message
$mol_test({
'print greeting to defined target'() {
const app = new $my_hello_message
app.target = ()=> 'Jin'
$mol_assert_equal( app.sub().join( '' ), 'Hello, Jin' )
} ,
})
Если же модулю нужны другие модули, то они заменяются заглушками и мы проверяем, что коммуникация с ними происходит как ожидается.
// Unit tests of $my_hello
$mol_test({
'contains Input and Output'() {
const app = new $my_hello
const Input = {} as $mol_string
app.Input = ()=> Input
const Output = {} as $mol_hello_message
app.Output = ()=> Output
$mol_assert_like( app.sub() , [
Input ,
Output ,
] )
} ,
'Input value binds to name'() {
const app = new $my_hello
app.$ = Object.create( $ )
const Input = {} as $mol_string
app.$.$mol_string = function(){ return Input } as any
$mol_assert_equal( app.name() , '' )
Input.value( 'Jin' )
$mol_assert_equal( app.name() , 'Jin' )
} ,
'Output target binds to name'() {
const app = new $my_hello
app.$ = Object.create( $ )
const Output = {} as $my_hello_message
app.$.$mol_hello_message = function(){ return Output } as any
$mol_assert_equal( Output.title() , '' )
app.name( 'Jin' )
$mol_assert_equal( Output.title() , 'Jin' )
} ,
})
Мокирование не бесплатно — оно ведёт к усложнению тестов. Но самое печальное — это то, что проверив работу с моками, вы не можете быть уверенными, что с реальными модулями всё это заработает правильно. Если вы были внимательными, то уже заметили, что в последнем коде мы ожидаем, что имя нужно передавать, через свойство title
. А это приводит нас к ошибкам двух типов:
И, наконец, тесты, получается, проверяют не требования (напомню — должно выводиться приветствие с подставленным именем), а реализацию (внутри вызывается такой-то метод с такими-то параметрами). А это значит, что тесты получаются хрупкими.
Хупкие тесты — такие тесты, которые ломаются при эквивалентных изменениях реализации.
Эквивалентные изменения — такие изменения реализации, которые не ломают соответствие кода функциональным требованиям.
Алгоритм TDD довольно прост и весьма полезен:
Если мы пишем хрупкие тесты, то на шаге рефакторига они будут постоянно падать, требуя исследования и корректировки, что снижает производительность программиста.
Чтобы побороть оставшиеся после модульных тестов кейсы, придумали дополнительный вид тестов — интеграционные. Тут мы берём несколько модулей и проверяем, что взаимодействуют они правильно:
// Integration tests of $my_hello
$mol_test({
'print greeting with name'() {
const app = new $my_hello
$mol_assert_equal( app.Output().sub().join( '' ) , 'Hello, ' )
app.Input().value( 'Jin' )
$mol_assert_equal( app.Output().sub().join( '' ), 'Hello, Jin' )
} ,
})
Ага, у нас получился тот самый последний компонентный тест. Иначе говоря, мы так или иначе написали все компонентные тесты, проверяющие требования, но дополнительно зафиксировали в тестах конкретную реализацию логики. Как правило это избыточно.
Criteria | Cascaded component | Modular + Integrational |
---|---|---|
CLOS | 17 | 34 + 8 |
Complexity | Simple | Complex |
Incapsulation | Black box | White box |
Fragility | Low | High |
Coverage | Full | Extra |
Velocity | High | Low |
Duration | Low | High |