image

Должен признаться: я читаю ACM Magazine. Это делает меня «ботаником» даже по меркам программистов. Среди прочего, я узнал из этого журнала о «метаморфическом тестировании». Раньше я никогда о нём не слышал, как и все люди, которых я спрашивал. Но научная литература по этой теме на удивление объёмна: есть множество невероятно успешных примеров её применения в совершенно разных областях исследований. Так почему же мы не слышали о нём раньше? Существует только одна статья для людей вне научных кругов. Пусть теперь их будет две.

Краткая предыстория


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

def test_dist():
    p1 = (0, 3)
    p2 = (4, 0)
    assert dist(p1, p2) == 5

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

Это приводит нас к генеративному тестированию (generative testing): написанию тестов, покрывающих случайное множество в пространстве состояний. Самым популярным стилем генеративного тестирования является property based testing, или PBT. Мы находим «свойство» (property) функции, а затем генерируем входные значения и проверяем, соответствуют ли выходные значения этому свойству.

def test_dist():
    p1 = random_point()
    p2 = random_point()
    assert dist(p1, p2) >= 0

Преимущество PBT заключается в покрытии большего пространства. Его недостаток — утеря специфичности. Это уже не оракул-тест! Мы не знаем, каким должен быть ответ, и функция может быть ошибочна, но таким образом, что обладает тем же свойством. Здесь мы полагаемся на эвристики.

Серьёзная проблема PBT — нахождение хороших свойств. У большинства функций есть простые, общие свойства и сложные, специфические свойства. Общие свойства могут применяться к большому множеству функций, но они не дают нам особо много информации. Более специфические свойства дают больше информации, но их сложнее найти и они применимы только в областях ограниченных задач. Если у вас есть функция, определяющая, является ли граф ациклическим, то какие property-тесты вы напишете? Дадут ли они вам уверенность в том, что функция верна?

Мотивация


Теперь рассмотрим более сложную задачу. Представьте, что вы хотите написать преобразователь речи в текст (speech-to-text, STT) для английского языка. Он получает звуковой файл, а выводит текст. Как бы вы его тестировали?

Проще всего использовать ручной оракул. Продиктовать предложение и проверить, совпадает ли с ним текст на выходе. Но этого даже близко недостаточно! Диапазон человеческой речи огромен. Лучше будет протестировать 1 000 или даже 10 000 разных звуковых файлов. Ручные оракулы с транскрипцией были бы слишком затратными. Это значит, что нам придётся вместо них использовать property-based testing.

Но как нам сгенерировать входные данные? Например, мы можем создать случайные строки, пропустить их через преобразователь текста в речь (text-to-speech, TTS), а затем убедиться, что наш STT выдаёт тот же текст. Но это опять же даёт нам очень ограниченный диапазон человеческого голоса. Сможет ли TTS создать изменения интонаций, «глотать» слова, имитировать сильный акцент? Если мы не справимся с ними, то будет ли STT особо полезен? Лучше использовать произвольные тексты, например, записи с радио, из подкастов и онлайн-видео.

Теперь возникает новая проблема. При использовании TTS мы начинали с письменного текста. В случае произвольных звуковых файлов у нас его нет, и в то же время мы не хотим выполнять транскрипцию вручную. Вместо этого мы ограничены использованием свойств. Какие же свойства нам нужно протестировать? Примеры простейших свойств: «программа не вылетает при любых входящих данных» (хорошее свойство) или «она не преобразует в слова акустическую музыку» (возможно?). Эти свойства не очень хорошо покрывают проверку основной задачи программы и слабо повышают уверенность в её качестве.

Итак, у нас есть две задачи. Во-первых, нам нужно большое множество входящих данных в виде речи. Во-вторых, нам нужно понять, как преобразовать их в полезные тесты, не тратя долгие часы на ручную транскрипцию голоса в оракулы.

Метаморфическое тестирование


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

  • Повышении громкости вдвое
  • Повышении частоты
  • Увеличении темпа
  • Добавлении фоновых помех
  • Добавлении шума транспорта
  • Любой комбинации из вышеперечисленного.

Всё это «простые» преобразования, которые мы можем легко протестировать. Например, для теста с «шумом транспорта» мы можем взять 10 сэмплов шума автомобилей, наложить их на звуковой клип и проверить, совпадают ли результаты распознавания всех 11 версий. Мы можем двое увеличить или уменьшить громкость, превратив 11 версий в 33 версий, а затем удвоить темп, получив 66 версий. Этот принцип можно применить к каждому звуковому клипу в нашей базе данных, существенно расширив пространство входящих данных.

Наличие 66 версий для сравнения довольно удобно. Но это ещё не всё: нам ещё и не нужно знать, какими должны быть выходные данные. Если все 66 преобразований возвращают out, то тест пройден успешно, если хотя бы одно возвращает что-то другое, то тест не пройден. Ни на одном из этапов нам не нужно проверять, что содержится в out. Это чрезвычайно важно. Так мы значительно увеличиваем тестируемое пространство с очень малым участием человека. Например, мы можем скачать эпизод сериала, выполнить преобразования и проверить, совпадают ли все результаты их преобразования в текст1. Мы получили полезные тесты, не прослушивая голосовой клип. Теперь мы можем генерировать сложные и глубокие тесты без использования оракула!

Два набора входящих данных, а также их выходящих данные связаны друг с другом. Такое свойство, относящееся к множеству входящих/выходящих данных называется метаморфической связью2. Тестирование, в котором применяется это свойство, называется метаморфическим тестированием. В сложных системах интересные метаморфические связи бывает найти проще, чем интересные свойства отдельных входящих/выходящих данных.

Изложим это чуть формальнее: если у нас есть x и f(x), то мы можем выполнить некие преобразования x, чтобы получить x2 и f(x2). В случае STT мы просто проверяем f(x) = f(x2), но можем использовать любые связи между этими двумя наборами данных. Могут быть и метаморфические связи типа f(x2) > f(x) или «является ли f(x2)/f(x) целым значением». Аналогичным образом можно расширить этот принцип на несколько наборов входящих данных, использовав f(x) и f(x3). Примером этого может быть сравнение результатов поискового движка без фильтров с результатами движка с одним или двумя фильтрами. В большинстве прочитанных мной описаний примеров использования применяется всего два набора входящих данных, потом что даже их достаточно для нахождения безумных багов.

Примеры использования


К слову о примерах использования: насколько эффективно метаморфическое тестирование на практике? Одно дело рассуждать о методике абстрактно или приводить искусственные примеры. Изучение примеров использования полезно по трём причинам. Во-первых, оно показывает, работает ли способ на самом деле. Во-вторых, из них можно узнать о потенциальных сложностях при использовании МТ. В-третьих, примеры показывают нам, как мы можем использовать методику. Любую метафорическую связь, применяемую в примере использования, можно попытаться адаптировать под решение наших задач.

В статье “Metamorphic Testing: A Review of Challenges and Opportunities” приводится список множества исследований, но все они являются научными статьями. Ниже представлены наиболее интересные. Статьи, помеченные (pdf), выложены, как можно догадаться, в PDF.

  • METTLE: A Metamorphic Testing Approach To Validating Unsupervised Machine Learning Methods (pdf). Здесь описываются 11 разных метаморфических связей для тестирования неконтролируемой кластеризации, например, «получим ли мы тот же результат, если перемешаем входящие данные?» и «принадлежат ли дополнительные входящие данные по краям кластеров к этим кластерам?» Различные модели изменяются при разных связях. Например, около 5% протестированных моделей k-средних при перемешивании порядка входящих точек имеют среднюю погрешность кластеризации 20%
  • DeepTest: Automated Testing of Deep-Neural-Network-driven Autonomous Cars (pdf). Тема статьи — системы зрения автопилотов в машинах. Примеры метаморфических связей (МС): «добавление фильтра дождя» или «небольшой наклон изображения». Авторы выложили результаты сэмплирования здесь: почти все протестированные ими системы не справлялись со своими функциями при изменении МС.
  • Automated Testing of Graphics Shader Compilers (pdf). Инъекции «мёртвого» кода и констант времени выполнения в шейдеры приводили к исчезновению объектов на изображениях или превращению их в шум. Исследовали на основании своей работы создали стартап GraphicsFuzz, который был куплен Google, а сайт закрыт.
  • Metamorphic Testing of RESTful Web APIs (pdf). Получим ли мы одинаковые элементы при изменении пагинации? Что если упорядочить их по дате? В этой статье приведена целая куча ошибок в Spotify и Youtube.
  • An innovative approach for testing bioinformatics programs using metamorphic testing (раньше pdf, но теперь нет). Поиск ошибок в биоинформационных данных. Я слабо разбираюсь в биоинформатике, но статья показывает, что МС полезны и в специализированных областях.

Проблема


О, так ведь все эти источники в PDF.

На поиск всех этих статей ушло несколько часов. И эта проблема связана с самым большим препятствием перед освоением МТ: все приведённые выше ссылки являются препринтами или первыми черновиками будущих научных статей. Когда я начинаю разбираться в малоизвестных методиках, то в первую очередь задаюсь вопросом: «почему они малоизвестны?» Иногда причина очевидна, иногда это сложное множество мелких причин, иногда проблема просто в том, что методике «не повезло».

В случае МТ проблема очевидна. Почти вся информация спрятана за научными paywall. Если вы хотите изучать МТ, вам или нужен доступ к журналу, или требуется потратить несколько часов на выискивание препринтов3.

Дальнейшее изучение


Изобретатель МТ — Ти Чен (Ty Chen). Он же стал движущей силой множества исследований. Другие исследователи в этой области — Ци Цюань Чжоу (Zhi Quan Zhou) и Серхио Сегура (Sergio Segura); оба они выложили все свои препринты в Интернет. Большинство исследовательских работ выполнено кем-то из этих людей.

Наверно, лучше всего начать изучение с Metamorphic Testing: A Review of Challenges and Opportunities и A Survey on Metamorphic Testing. Хоть эта статья написана о метаморфическом тестировании, исследовали также применили метаморфические связи в целом к широкому множеству других дисциплин, например, к формальной верификации кода и отладке. Я пока подробно не изучал эти области применения методики, но, вероятно, на них тоже стоит взглянуть.

С точки зрения применимости, теоретически может оказаться возможным адаптировать большинство PBT-библиотек к проверке метаморфических свойств. На самом деле первый пример в Quickcheck тестирует МС, а в этом эссе по PBT косвенно применяются МС. В целом мне кажется, что большинство исследований PBT фокусируется на эффективной генерации и усечении входящих данных, а исследования МТ в основном сосредоточены на определении того, что нам нужно тестировать на самом деле. Следовательно, эти методики, вероятно, взаимно дополняют друг друга.

Благодарю Брайана Ына (Brian Ng) за помощь в исследованиях.

Постскриптум: просьба


На самом деле неудивительно, что я никогда не слышал раньше об этой методике. Существует множество действительно интересных и полезных техник, которые не смогли покинуть своего крошечного пузыря. Я узнал об МТ скорее благодаря везению, а не активным поискам.

Если вам известно что-то, стоящее широкого применения, то напишите мне, пожалуйста.



  1. Ну ладно, тут могут быть очевидные проблемы: в подкасте может быть музыка, фрагменты речи на других языках и т.д. Но теория надёжна: если мы способны получить сэмплы речи, то можем использовать их как часть тестов без предварительной ручной транскрипции/разметки.
  2. В спецификациях соответствующей идеей являются гиперсвойства (hyperproperties) — свойства множеств поведений, а не отдельных поведений. Большинство исследований гиперсвойств связано с ГС безопасности. Насколько я понял, её ГС являются надмножеством МС.
  3. У меня была вторая, теперь опровергнутая, гипотеза: так как большинство основных исследователей из Китая и Гонконга, возможно, эта методика более известна в сообществах программистов, общающихся на мандаринском, а не английском. Брайан Ын проверил для меня эту гипотезу, но не обнаружил никаких существенных признаков применения методики китайцами.

Комментарии (9)


  1. Andrey_Volk
    03.06.2019 09:34
    +1

    Теория это конечно круто, но было бы неплохо посмотреть на пример такого теста.


  1. Yenox
    03.06.2019 09:53

    Есть некоторое пересечение с аугментацией — расширении обучающего датасета на задачах класса Machine Learning. Тоже берется небольшое количество данных и увеличивается. Например изображение котика масштабируется, сдвигается, другим способом корежится — и соответственно меняется описание того, что в данном элементе датасета (размеры объекта в Kitti-файле для задач Computer Vision)


  1. kahi4
    03.06.2019 10:02

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

    ИМХО, он не популярен не потому что научные статьи за деньги, а потому что с одной стороны есть широкая тема контрактного программирования, которая про тоже самое, с другой — такие тесты сложно написать и не всегда уместны. С тем же анализом голоса: ну нейронная сеть будет отвечать на все подряд, такой тест ничего не тестирует. Он не проверяет — упадёт ли она, если ей пол часа начитывать, и все такое. Скачивать каждый раз случайный ролик — это круто, но на практике подобные зависимости от внешних сред злы, потому что никто не любит когда их тесты падают через раз, пооому что внешний сервис может стать недоступным и ваши тесты все.


    1. neurocore
      03.06.2019 10:40

      Ловит особые кейсы, когда система совсем начинает бредить. Зато охват значений большой.


      1. kahi4
        03.06.2019 12:13

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


        1. infinum1
          04.06.2019 05:03

          Наверное, имеется в виду "фаззинг": https://ru.m.wikipedia.org/wiki/Фаззинг


  1. Sane
    03.06.2019 12:54

    Есть замечательный ScalaCheck (https://habr.com/ru/post/319456/), например.


  1. gasizdat
    03.06.2019 19:21

    out может быть одним и тем же во всех метаморфозах, но при этом ошибочным (например сообщением об ошибке распознавания речи).


  1. lxsmkv
    04.06.2019 04:20

    В тестировании есть два вопроса — «что» и «как». В чем мы хотим убедиться. И как мы можем в этом убедиться. Оба вопроса нетривиальны. Если на вопрос «что» мы еще можем ответить систематическим, рациональным, «инженерным» размышлением, то на вопрос «как» часто может просто не быть исчерпывающего ответа.
    Как убедиться в том, что поиск по запросу «кошки» выдает только кошек? Проверять каждую картинку с помощью ИИ? А подбор выдачи картинок уже был создан с помощью ИИ. Все — замкнутый круг.

    Вспоминается теорема Райса гласящая

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

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