Описание задачи
Оценка генерации юнит-тестов для функций и методов на пяти языках программирования (Java, Python, Go, JavaScript и C#). Датасет содержит 2500
задач.
Тестируемые навыки моделей: Instruction Following, Long Context Comprehension, Synthesis, Testing
Авторы: Алена Пестова, Валентин Малых
Мотивация
Юнит-тестирование - это важная практика разработки программного обеспечения, при которой отдельные компоненты программной системы оцениваются изолированно.
Данный бенчмарк направлен на оценку способностей моделей генерировать юнит-тесты для методов и функций на пяти языках программирования - Java, Python, Go, JavaScript и C#.
Задача генерации юнит-теста формулируется следующим образом: дана функция или метод (далее фокальная/тестируемая функция/метод), и необходимо сгенерировать юнит-тест для нее (далее - тестовая функция/метод/тест).
Датасет ориентирован на инструктивные кодовые и мультимодальные модели.
Результаты оценки могут быть полезны:
- исследователям в области автоматической генерации кода в целом и юнит-тестов в частности
- разработчикам инструментов для автоматической генерации кода
На основании результатов можно будет сделать вывод о том, насколько сгенерированные моделью тесты схожи с тестами, написанными людьми.
Задача оценивает способность модели генерировать юнит-тесты для данной функции/метода, учитывая дополнительный контекст, собранный из проекта. Таким образом, также проверяется способность генерации кода на уровне проекта, что важно при использовании моделей для генерации юнит-тестов для реальных проектов.
Модели предоставляется инструкция, которая содержит
- задачу (сгенерировать юнит-тест)
- используемый язык программирования
- текст метода/функции для тестирования,
- путь к файлу метода/функции для тестирования, а также остальной код из этого файла
- путь к файлу, где будет лежать сгенерированная тестовая функция
- (опционально) тестовый фреймворк, который необходимо использовать
- тип юнит-теста для генерации - метод или функция
- дополнительный контекст из как бы существующего будущего тестового файла
Про дополнительный контекст из тестового файла:
Так как датасет собирался из файлов реальных репозиториев с гитхаба, в тестовых файлах проектов содержится не одна тестовая функция, а также могут быть необходимые импорты, переменные, вспомогательные функции и методы, а также сами тестовые функции на разные юниты. Понятно, что в базовом сценарии мы можем просто дать модели юнит (и даже какой-то дополнительный контекст для этого юнита) и попросить сгенерировать для него тест. Однако в таком случае появляется проблема, что модель не видела контекста из тестового файла. Если такие данные используются для обучения, то мы намеренно учим модель галлюцинировать, а именно испоьзовать в тестовой функции какие-то библиотеки, другие функции и классы, которые нигде не были описаны ранее. Если мы используем такие данные для тестирования, то получается, что мы предоставили модели очень ограниченный контекст и сравнение с человеческими тестами не самое честное.
Можно было бы просто давать модели весь остальной текст из тестового файла и просить сгенерировать таргетную тестовую функцию, но тогда появляется большая вероятность утечки данных - где-то в файле могла использоваться данная тестовая функция, к тому же другие тестовые функции тоже могли попасть в такой контекст. Поэтому нами было принято решение собирать из тестовых файлов некоторый урезанный контекст для каждой тестовой функции и передавать его модели.
Метрикой оценки качества является CodeBLEU, оценивающая схожесть сгенерированного теста и теста, написанного человеком.
Описание датасета
Поля датасета
instruction
[str] — Промпт-инструкция для модели, содержащая шаблон для вставки элементов вопроса.inputs
— Вводные данные, формирующие задание для модели. Могут включать одну или несколько модальностей - видео, аудио, изображение, текст.focal_func
[str] — тестируемая функция/метод для тестирования;test_func_type
[str] — тип теста (function/method);test_func_context
[str] — контекст тестовой функции, собранный из тестового файла - импорты, глобальные переменные, краткое описание других функций/классов;language
[str] — язык программирования (python, java, csharp, js, go);focal_file_path
[str] — путь к файлу тестируемой функции/метода в исходном репозитории;test_file_path
[str] — путь к файлу тестовой функции/метода в исходном репозитории;focal_func_context
[str] — контекст фокальной функции: текст фокального файла, откуда вырезан сам тестируемый метод и заменен на текст `#focal function/method here`;test_framework
[str] — тестовый фреймворк, который необходимо использовать (только для JS);
outputs
[str] — Правильный ответ на вопрос.meta
— Метаданные, относящиеся к тестовому примеру, но не используемые в вопросе (скрытые от тестируемой модели).id
[int] — Номер-идентификатор вопроса в датасете.repo_id
[str] — Идентификатор репозитория;focal_func_type
[str] — тип тестируемоего объекта(function/method)
Промпты
Для задачи были подготовлены 20 промптов, которые были равномерно распределены по вопросам по принципу "один вопрос – один промпт". Шаблоны в фигурных скобках в промпте заполняются из полей внутри поля inputs
в каждом вопросе.
Пример промпта:
"Сгенерируйте функцию на языке {language}.
Напиши тест для этого кода на языке {language} из файла '{focal_file_path}'.
Вот код, который надо протестировать:
{focal_func}
Тебе необходимо написать {test_func_type} на языке {language}. Тест будет помещен в файл '{test_file_path}'.
Обязательно учитывай код, собранный из будущего тестового файла:
{test_func_context}
Для тебя собран код из репозитория, который может помочь тебе в написании теста:
{focal_func_context}
Напиши только {test_func_type} без пояснений и комментариев. Не забывай соблюдать синтаксис языка {language}.
Оформи свой ответ с соблюдением markdown разметки для кода:```{language}
<your code>```
Создание датасета
Процесс сбора датасета состоял из следующих этапов:
1. Парсинг списка репозиториев, фильтрация списка и скачивание репозиториев.
2. Парсинг репозиториев, функций, методов и тестов
3. Сопоставление методов/функций и соответвующих им тестов.
Далее будут подробнее описаны эти этапы.
Список репозиториев для каждого языка был получен с помощью GitHub API. Мы выбрали репозитории только с открытыми лицензиями и те, у которых больше 10 звезд. Мы также отфильтровали fork-репозитории. Список используемых лицензий: MIT License, Apache License 2.0, The Unlicense, Mozilla Public License 2.0, BSD 2-Clause "Simplified" License, BSD 3-Clause "New" or "Revised" License, EPL 1.0 license, EPL 2.0 license, MPL 2.0 License, Unlicense License, 0BSD license.
При построении набора данных использовались одинаковые правила фильтрации для всех языков:
+ Пустые тесты удалены.
+ Из одного репозитория было собрано не более 200 пар метод-тест. Если пар было больше, они отбирались случайным образом.
+ Тестовый пример должен содержать не более 5000 символов. Это ограничение установлено для удаления из данных слишком длинных тестов.
+ Максимальная длина входных данных (тестируемая функция с контекстом) должна составлять не более 70000 символов.
+ Максимальное количество проверок (слово "assert" в тестовом примере) равно 20.
+ Для Python и Java была реализована дополнительная фильтрация тестов с синтаксическими ошибками (с использованием библиотек ast и javalang соответственно).
+ Обучающие данные были отфильтрованы на наличие дубликатов.
Для всех языков (кроме Python) для разбора кода использовался tree-sitter, в частности, для поиска и разбора функций/методов и классов, идентификации вызовов и т.д. Для Python мы использовали встроенную библиотеку ast.
После того, как мы спарсили все классы, методы и функции в репозитории, необходимо как-то понять, какой именно метод/функцию тестируют тестовые функции. То есть необходимо их сопоставить и сформировать список метод-тест. Методы/функции и юнит-тесты для них были сопоставлены с использованием метода, адаптированного из статьи (в работе сопоставляли только методы и тесты на Java). Далее будет кратко описано, как мы адаптировали этот метод для каждого языка.
+ В Java тестовые классы сопоставляются с фокальными классами по их путям в репозитории и именам. Затем методы фокального и тестового классов сопоставляются с помощью двух эвристик - имен и уникальности вызова метода.
+ Для Python все проанализированные функции и методы были сопоставлены с тестами в соответствии с правилами именования тестов в pytest. Исходя из логики, согласно которой одна функция может быть протестирована несколькими тестами, но один тест предназначен только для одной функции, в набор данных добавляются только тесты, соответствующие одной функции/методу.
+ Для Go идентификация тестовых функций и сопоставление их с фокальными функциями были выполнены в соответствии с практиками именования тестовых файлов и функций в библиотеке testing. Процедура сопоставления тестов была выполнена таким же образом, как и для Python.
+ Для C# фокальные и тестовые методы объединяются в пару, если название тестового метода включает в себя название фокального метода из репозитория и вызывает этот метод. В набор данных добавляются только тесты, сматченные с одним фокальным методом.
+ Для JavaScript также был проанализирован тестовый фреймворк, используемый в репозитории, путем поиска одной из следующих библиотек в зависимостях в файле "package.json": "mocha", "jest", "jasmine", "qunit", "nightwatch". Впоследствии название фреймворка было добавлено во инпут модели как одна из частей контекста тестового файла. В отличие от других языков, это необходимо, поскольку импорт часто не содержит информации о тестовом фреймворке. Если тестовый фреймворк из списка не был найден в зависимостях репозитория, тестовая функция все равно добавлялась в набор данных, но тестовый фреймворк определялся как "Unknown". Что касается сопоставления метода и теста, то это единственный язык, где оно было основано только на последнем вызове локального метода/функции, поскольку тестовые функции не имеют идентификаторов при объявлении в it() и test().
Метрики
Для агрегированной оценки ответов моделей используются следующие метрики:
CodeBLEU
: Code BLEU учитывает соответствие двух частей кода, аналогичное исходному BLEU, но может также учитывать грамматическую и логическую корректность, используя абстрактное синтаксическое дерево и структуру потока данных.