Концепции автоматического тестирования

Здравствуйте, меня зовут Дмитрий Карловский и у меня, к сожалению, нет времени писать большую статью, но очень хочется поделиться некоторыми идеями. Поэтому позвольте потестировать на вас небольшую заметку о программировании. Речь сегодня пойдёт об автоматическом тестировании:


  1. Зачем мы пишем тесты?
  2. Какие бывают тесты?
  3. Как мы пишем тесты?
  4. Как их стоит писать?
  5. Почему модульные тесты — это плохо?

Правильная пирамида тестирования


Задачи автоматического тестирования


От более важного к менее:


  1. Обнаружение дефектов как можно раньше. До того как увидит пользователь, до того как выложить на сервер, до того как отдать на тестирование, до того как закоммитить.
  2. Локализация проблемы. Тест затрагивает лишь часть кода.
  3. Ускорение разработки. Исполнение теста происходит гораздо быстрее ручной проверки.
  4. Актуальная документация. Тест представляет из себя простой и гарантированно актуальный пример использования.

Ортогональные классификации


  1. Классификация по объекту
  2. Классификация по типам тестов
  3. Классификация по видам процесса тестирования

На всякий случай подчеркну, что речь идёт исключительно про автоматическое тестирование.


Объекты тестирования


  1. Модуль или юнит — минимальный кусок кода, который можно протестировать независимо от всего остального кода. Тестирование модулей так же известно как "юнит-тестирование".
  2. Компонент — относительно самостоятельная часть приложения. Может включать в себя другие компоненты и модули.
  3. Приложение или система — вырожденный случай компонента, косвенно включающего в себя все остальные компоненты.

Типы тестов


  1. Функциональные — проверка соответствия требованиям функциональности
  2. Интеграционные — проверка совместимости соседних объектов тестирования
  3. Нагрузочные — проверка соответствия требованиям производительности

Виды процессов тестирования


  1. Приёмочный — проверка новой/изменённой функциональности.
  2. Регрессионный — проверка отсутствия дефектов в не изменявшейся функциональности.
  3. Дымовой — проверка основной функциональности на явные дефекты.
  4. Полный — проверка всей функциональности.
  5. Конфигурационный — проверка всей функциональности на разных конфигурациях.

Количество тестов


  • Тесты — это код.
  • Любой код требует времени на написание.
  • Любой код требует время на поддержку.
  • Любой код может содержать ошибки.

Чем больше тестов, тем медленнее идёт разработка.


Полнота тестирования


  • Тесты должны проверить все пользовательские сценарии.
  • Тесты должны зайти в каждую ветку логики.
  • Тесты должны проверить все классы эквивалентности.
  • Тесты должны проверить все граничные условия.
  • Тесты должны проверить реакцию на нестандартные условия.

Чем полнее тесты, тем быстрее идёт рефакторинг и тестирование, и как следствие поставка новой функциональности.


Бизнес приоритеты


  1. Максимизация скорости разработки. Разработчику надо писать минимум тестов, которые быстро исполняются.
  2. Минимизация дефектов. Надо обеспечивать максимальное покрытие.
  3. Минимизация стоимости разработки. Надо тратить минимум усилий на написание и поддержку кода (в том числе и тестов).

Стратегии тестирования


В зависимости от приоритетов, можно выделить несколько основных стратегий:


  1. Качество. Пишем функциональные тесты на все модули. Проверяем их совместимость интеграционными тестами. Добавляем тесты на все невырожденные компоненты. Не забываем и про интеграционные для компонент. Присыпаем тестами всего приложения. Многоуровневое исчерпывающее тестирование потребует много времени и ресурсов, но позволит с большей вероятностью выявить дефекты.
  2. Скорость. Используем лишь дымовое тестирование приложения. Мы точно знаем, что основные функции работают, а остальное починим, если вдруг. Таким образом мы быстро поставляем функциональность, но тратим много ресурсов на доведение её до ума.
  3. Cтоимость. Пишем тесты лишь на всё приложение. Критичные дефекты таким образом обнаруживаются заблаговременно, что позволяет снизить стоимость поддержки и как следствие относительно высокую скорость поставки новой функциональности.
  4. Качество и скорость. Покрываем тестами все (в том числе вырожденные) компоненты, что даёт максимальное покрытие минимумом тестов, а следовательно минимум дефектов при высокой скорости, в результате давая и относительно низкую стоимость.

Пример приложения


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


$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 просто создаёт экземпляр и переопределяет указанные свойства.


Компонентное тестирование


При этом подходе мы используем реальные зависимости всегда, когда это возможно.


Что следует мокать в любом случае:


  1. Взаимодействие со внешним миром (http, localStorage, location и тп)
  2. Недетерминированнось (Math.random, Date.now и тп)
  3. Особо медленные вещи (вычисление криптоскойкого хэша и тп)
  4. Асинхронность (синхронные тесты проще в понимании и отладке)

Итак, сперва пишем тест на вложенный компонент:


// 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. А это приводит нас к ошибкам двух типов:


  1. Правильный код модуля может давать ошибки на моках.
  2. Дефектный код модуля может не давать ошибки на моках.

И, наконец, тесты, получается, проверяют не требования (напомню — должно выводиться приветствие с подставленным именем), а реализацию (внутри вызывается такой-то метод с такими-то параметрами). А это значит, что тесты получаются хрупкими.


Хрупкие тесты — такие тесты, которые ломаются при эквивалентных изменениях реализации.

Эквивалентные изменения — такие изменения реализации, которые не ломают соответствие кода функциональным требованиям.

Test Driven Development


Алгоритм TDD довольно прост и весьма полезен:


  1. Пишем тест, убеждаемся, что он падает, что означает, что тест реально что-то тестирует и изменения в коде реально необходимы.
  2. Пишем код, пока тест не перестанет падать, что означает, что мы выполнили все требования.
  3. Рефакторим код, убеждаясь, что тест не падает, что означает, что наш код по прежнему соответствует требованиям.

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


Интеграционные тесты


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


// 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

Ссылки по теме


Поделиться публикацией
Ой, у вас баннер убежал!

Ну. И что?
Реклама
Комментарии 43
    +4
    Привет, Дмитрий.

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

    Также не понял, для кого статья предназначалась. Если для новичков в теме, то снова возвращаемся к ее скомканности. Если для людей, которые в целом в теме, то столь подробная структура излишняя — она не несет ничего нового.

    В общем и целом: статьи на Хабре по большей части я бы разделил на два виды. Либо статья описывает какую-то проблему и ее решение. Либо рассказывает подробно о чем-то (как что-то сделать, как чем-то пользоваться и т.д.). Данная статья не решает никакой заявленной в начале проблеме, но и на обучающую тоже не тянет.

    Думаю, если найти все же время, в дальнейшем можно развить статью во что-то клевое. Успехов! :)
      0
      Также не понял, для кого статья предназначалась.

      Для меня:). Как всегда Дмитрий выражает мысли лаконично, но вместе с тем предельно чётко. Если в голове нет сумбура, то и в статье откуда ему взяться???

        +1
        Если же модулю нужны другие модули, то они заменяются заглушками и мы проверяем, что коммуникация с ними происходит как ожидается.

        Это только если вы так решили. А можно, наоборот, подставить модуль, который ведет себя так, как ожидает тестируемый, и проверять поведение тестируемого.


        Другое дело, что иногда цель модульного тестирования — это и есть проверка комуникации с зависимостями, и тогда без моков вы не обойдетесь.

          0
          Разве в этом случае не получится интеграционный тест?
            0

            Нет, почему?

          +1

          Есть манки патчинг для тестирования объектов он идеален

            0

            … где есть-то? Где-то есть, а где-то его нет, или он избыточно сложен.

            –2

            Его проблематично использовать в ФП, но в ООП он соответствует сложенности объекта

              0

              То есть, по вашему мнению, от языка/экосистемы это никак не зависит?


              Что же такого сложного в monkey patch в функциональном языке?

                –1

                "Это" это что такое? Сформируй свой вопрос более сосредоточенно.

                  0

                  "Это" — это сложность реализации monkey patching в тесте (и вообще сложность юнит-тестирования с использованием monkey patching).

                    0

                    Тестирование это верификация работоспособности модулей на основе контрольных значений. Расстановка и обработка этих значений в ООП удобнее. Ну на мой взгляд. Что касается сложности. Сложность определяется количеством связей. Уменьшите количество связей уменьшите сложность и наоборот.

                      0
                      Тестирование это верификация работоспособности модулей на основе контрольных значений. Расстановка и обработка этих значений в ООП удобнее.

                      … вот только с monkey patching это все никак не связано.

                        –1

                        У тебя есть другое название?

                          0

                          Для какого явления?

                            0
                            Расстановка и обработка контрольных значений в ООП
                              0

                              Собственно, вы же и процитировали "другое название".

                                0

                                а манки патчить значения в ООП вам религия не позволяет?

                                  0

                                  Ну во-первых, если это можно сделать без monkey patch, то да, религия гласит, что лучше сделать без monkey patch.


                                  Во-вторых, покажите, пожалуйста, как сделать monkey patch в .net для банального Stream.Length.

                                    –1

                                    Я не знаю и не хочу знать.

                                      0

                                      Ну вот тогда и не утверждайте, что в ООП monkey patch идеален для тестирования объектов. Возможно, в каком-то известном вам языке/экосистеме — да. Но не в ООП в целом.

                                        –1

                                        Утверждаю — идеален на любом языке. Ибо язык собственно не причём. Почитайте на досуге про верификацию компиляторов.

                                          0
                                          Утверждаю — идеален на любом языке.

                                          Утверждать-то вы можете что угодно, но вот только доказать это не можете.


                                          Ибо язык собственно не причём.

                                          И что же делать, если язык (точнее, среда выполнения) не позволяют monkey patching?

                                            –1

                                            Вам что-то доказывать это только время терять.

                                              0

                                              Хорошая попытка, но нет.

                                            0
                                            Утверждаю — идеален на любом языке. Ибо язык собственно не причём. Почитайте на досуге про верификацию компиляторов.

                                            В зависимости от языка и среды исполнения вам придется или специально проектировать код для такого механизма или извратиться так, что усилия превысят разумые пределы.
                                            У вас есть С++ — в добрый путь :)
                                            в .Net тоже можно извратиться весьма сходно — но «цена» и сложность превысит сложность проверяемого кода.
                                              –1

                                              Да надо стараться минимизировать количество тестируемого контекста, а количество контента максимизировать, ну эт общее правило которое справедливо для обоих подходов.

                –1
                маленькая стрелочка в последнем ряду так была и задумана?
                на картинке вторая слева
                  0
                  Модуль или юнит — минимальный кусок кода, который можно протестировать независимо от всего остального кода. Тестирование модулей так же известно как «юнит-тестирование».

                  1. А насколько независимо от остального кода? какой критерий «независимо»? Например код типа «трансформация данных» или «парсер»/сериализатор/десериализатор к какому будет относится типу?
                  2. К какому типа будет относится проверка «workflow» если вопрос стоит «корректный да-нет»?
                  Unit -Component — Integration?
                    –1
                    Локализация проблемы. Тест затрагивает лишь часть кода.

                    из своего опыта скажу что юнит тесты даже если покрывают 100% кода ещё не значит что отдельные компоненты будут работать вместе правильно, а ещё может быть избыточность действий когда разные модули слишком большие и они делают одно и тоже но немного по разному, либо вовсе не работают вместе, так может вместо юнит тестов писать сразу функциональные которые делают тоже что и конечный продукт в целом, тестов получается меньше проверяют они столько же, бегут правда помедленнее, но без них всё равно никак а 2 раза тестировать сначала юнит потом функциональными помоему глупо, если прилепить базу так ещё и нагрузочные тесты можно замутить, плохо лишь одно что надо будет хелперы писать чтоб подготовить состояние для самого теста, но опять же это заставляет думать о проблеме в целом, а не только независимый кусочек, если проэкт огромный то тогда ничё не поделаешь и надо модульно тестировать, но всё равно моков поминимуму и только для совсем внешних сервисов которые нельзя из тестов использовать

                    у меня такой подход отлично работает разве что уи не тестируется, да и то только потому что автоматизаторы сами должны были под это тесты написать, получается что не надо кнопки тыкать, а писать тест на требуемую ситуацию и под этот тест уже писать код, думать почти не надо как и чего сделать в коде, а ещё и всякие рантайм ошибки сразу вылезают связаные с байндингом и тому подобным, а значит нет человеческих ошибок типа забыл чегото там доконфить, которая вылезет только у тестеров
                      0
                      проверяют они столько же

                      Главная ошибка
                        0
                        в чём ошибка то?
                          0
                          В том, что функциональные тесты не проверяют работу приложения полностью.
                          даже если покрывают 100%

                          Юнит-тестирование, на то и «Юнит». Чтобы отдельно проверить логически завершённый сегмент всевозможными данными, пройти по всем вилкам условий, выкинуть и отловить все Exception-ы.
                          Только после юнит-тестирования имеет смысл делать функциональные (как работает весь конвейер).

                          PS. Если юниттесты долгие, советую использовать заглушки модулей которые пытается вызвать тестируемая часть. Если ваш ЯП поддерживает monkey patching из коробки то задача упрощается до безобразия.

                          PPS. Если код плохо поддаётся тестированию — то код плохо написан
                            0

                            так, а где обьяснение? это всё и функциональными делается, валидаторы и места где 100500 вариантов естественно проще юнит тестами, но не обязательно всё зависит от сложности написания и скорости их работы, ведь важно в конце концов как всё вместе работает, и функциональные сложно писать и бегут долго в огромных сложных проэктах, там начальное состояние сложно создать, например софт который считал накопительную пенсию из данных за 30 лет, но даже тут только первый раз было сложно потому что данных дофига надо создать, работало бы быстрее юнит тесты вообще не писались бы

                              0
                              так, а где обьяснение?

                              Объяснение, прошу прощения, чего?
                              Того что функциональные не дают 100% покрытия? Так я объяснил, конвейер снижает вариативность поступающих данных (часть мутирует, часть валидируется). Функциональные тесты не выбрасывают половину Exception-ов глуша и обрабатывая их выше (в лучшем случае).
                              Юнит тесты сразу и точно показывают где что-то сломалось…

                              Есть у нас 100 модулей, каждый из которых прибавляет ко входящему параметру 1. Модули объединены в один конвейер. Покрываются одним функциональным тестом.
                              Правим 1 модуль теперь он добавляет 2 к параметру. Тест упал. В каком модуле произошла ошибка?
                              Ладно, это просто. Нашли в гите, поправили.

                              Два разработчика поправили по модулю. один теперь добавляет 0, второй добавляет 2. Ошибки компенсировали друг-друга. Тест пройден. Но приложение содержит ошибку. И когда эта мина подорвётся?

                              Во второй части своего комментария вы описываете необходимость функционального тестирования. С этим я не спорил
                                0
                                вот тут как раз таки и есть проблема избыточности, юнит тестом можно передать всякие невалидные данные которые никогда не дойдут то этого метода и из 10 строчек можно превратить всё в 100 строчек, а это вовсе не нужно, всю работу уже сделали другие модули

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

                                последнее замечание не совсем валидно если код даёт в конце правильный результат и покрыт на 100% то и не важно что они компенсируют друг друга и всегда дадут нужный результат то что один модуль доделывает работу другого не критично(от перемены мест слагаемых сумма не меняется), ну по крайней мере до тех пор пока в этом коде ничего не меняется, а как будет меняться то и ошибка вылезет и будет исправлена

                                вобщем я понял вашу мысль и я не являюсь ярым противником юнит тестов их нужно писать где по другому сложнее так как они сократят время разработки, но нельзя забывать что чем больше тестов тем больше времени на их поддержку, и можно потерять всё сьэкономленное время на их переписывание и рефакторинг, был я в таких проэктах где основной упор был на юнит тесты (тысячи тестов) и в таких где на функциональные(несколько сотен) и с функциональными проект был менее проблемным, там где больше юнит тестов было получалось что всё покрыто и вроде должно работать, а всё вместе иногда не работало как надо
                                  0
                                  последнее замечание не совсем валидно если код даёт в конце правильный результат

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

                                  Только ситхи все возводят в абсолют
                      0

                      Программы должны тестироваться и выполняться одним движком. Также как это делает компилятор. Тогда или тестеры или программисты будут не нужны. А пока одни пишут код с ошибками, а другие их находят, трудоемкость ручного тестирования с увеличением сложенности будет только возрастать.
                      Писать такие проги непросто. Потому что этому в школе/институте не учат. С другой стороны задач которые не требуют спецподготовки на порядки меньше. Видимо поэтому и спрос на такие комплексные решения тоже меньше. Этот эсеншл не для всех.

                        0
                        Программы должны тестироваться и выполняться одним движком. Также как это делает компилятор. Тогда или тестеры или программисты будут не нужны.

                        … как из одного вытекает другое, простите? Откуда "движок" возьмет сценарии тестирования?

                        –1

                        Не прощу:) Откуда их берет компилятор? А там сплошное исполнение умноженное на тестирование. Если код не предусмотрел эксепшн, то тест учтёт только потому что сначала проверит то множество контрольных значений и условий которое ты ему предоставишь.

                          0
                          Откуда их берет компилятор?

                          Из спецификации на язык. А вот спецификацию на ваш код компилятору никто предоставлять не будет.

                          –1

                          Тебе лишь бы языком помолоть, пустомеля

                            0

                            … ну то есть опять аргументов нет.

                          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                          Самое читаемое