Что пишут в блогах

Подписаться

Что пишут в блогах (EN)

Разделы портала

Онлайн-тренинги

.
Настраиваем собственные инструменты: тестирование подсветки кода в IDE
21.04.2025 00:00

Автор: Томаш Балог (Tamás Balog)
Оригинал статьи
Перевод: Ольга Алифанова

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

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

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

Возможность статически анализировать код особенно важна для разработчиков редакторов кода и IDE (интегрированных сред разработки), а также для тех, кто создает IDE-плагины. Это связано с тем, что создатели языков программирования, тест-фреймворков и т. п. должны внедрять статический анализ кода для его доступности инженерам-пользователям.

Однако просто внедрить такие проверки недостаточно. Очень важно протестировать их, чтобы убедиться в их верной работе – они не должны путать пользователей или вводить их в заблуждение, ложно сообщая о правильном коде, как о неправильном (и наоборот).

Немного предыстории

Прежде чем я начал разрабатывать плагины для JetBrains IDE, я работал тест-автоматизатором. Я писал юнит-тесты для наших фреймворков тест-автоматизации, интеграционных Selenium-тестов и тестов Web UI.

Когда меня познакомили с тестированием IDE-плагинов, мне поначалу показалось очевидным, что для ряда фич нужны юнит-тести и имитация объектов. Это было не слишком сложно, но постепенно я осознал, что это перебор. Если у вас есть только молоток – все вокруг становится похожим на гвоздь.

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

Разные IDE-платформы – разные возможности тестирования

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

Мой опыт в разработке расширений для Visual Studio и VS Code ограничен (я в основном работаю над плагинами к JetBrains IDE), и, вероятно, есть решения получше, чем приведенные мной. Однако они все же демонстрируют то, о чем я говорил во введении.

Пожалуйста, обратите внимание: секции ниже содержат код на трех разных языках - C#, TypeScript и Java, поэтому потребуются некоторые технические знания. Я старался, чтобы код был как можно проще и чище.

Итак, начнем с примера.

Пример проблемы

Допустим, у нас есть простой метод с единственным параметром vararg, в который должно передаваться четное число пар имя-значение.

Пример для C#:

void SetHeaders(params string[] nameValuePairs) { ... }

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

anObject.SetHeaders("header", "value", "another header");

Посмотрим на пример решения в расширении Visual Studiol

Расширения Visual Studio

Начну с упрощенного объяснения, как расширения Visual Studio работают с подчеркиванием различных частей документа.

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


Волнистое подчеркивание представлено, как класс ErrorTag и его конкретные специфические реализации. Оно несет видимую пользователю информацию – тип ошибки для форматирования, и подсказку (к примеру, сообщение об ошибке) для отображения.

Для отображения ErrorTags они должны быть зарегистрированы в редакторе. Это делается при помощи так называемых копий ITagger, с собственным ITagger для каждого типа тегов через метод GetTags():

public IEnumerable<ITagSpan<SquiggleTag>> GetTags(NormalizedSnapshotSpanCollection spans) { ... }

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

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

Тестирование данных тегирования

Мы знаем, что для получения и последующей валидации актуальной информации о тегах надо вызвать ITagger.GetTags(NormalizedSnapshotSpanCollection). Это будет выглядеть примерно так:

IList<ITagSpan<IErrorTag>> squiggles = squiggleTagger.GetTags(spanCollection).ToList();

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

Вызвав getTags(), можно проверить возвращенные данные о волнистом подчеркивании. Мы знаем, что в сниппете кода выше должен быть только один тег – на имени метода. Начнем с его проверки:

Assert.That(squiggles, Has.Count.EqualTo(1));

Затем валидируем все данные в коллекции: стартовую позицию, длину подчеркивания, тип ошибки и сообщение об ошибке:

Assert.That(squiggles[0].Span.Start.Position, Is.EqualTo(120));
Assert.That(squiggles[0].Span.Length, Is.EqualTo(10));
Assert.That(squiggles[0].Tag.ErrorType, Is.EqualTo("Error"));
Assert.That(squiggles[0].Tag.ToolTipContent, Is.EqualTo("An even number of arguments must be passed in."));

Дальше в тесте будет создание или имитация SpanCollection, которая передается в GetTags(), и другая логика настройки вроде эмуляция документа, чтобы он был не пустым, и некое визуальное отображение местоположения тестируемых тегов.

Хорошее ли это решение?

Как можно видеть, по сути это юнит-тестирование, и само по себе это не проблема. Но несмотря на то, что цели тестирования тегов мы достигаем, я вижу тут ряд проблем:

  • Излишняя гранулярность. Конечно, проверки всегда можно спрятать в служебных методах, но суть тестов останется неизменной.
  • Не то чтобы масштабируемо, если нужно валидировать другие специфические данные о тегах или множество иных тегов.
  • Плохо передает цель – мы не видим, где будут отображаться теги в документе.
  • Более того, при изменении содержимого тест-документа для нового теста придется обновлять имеющиеся позиции для тегов в совершенно не связанных тестах.

Должен подчеркнуть, что для разработки VS-расширений есть куда более продвинутые решения – скажем, проект dotnet/roslyn. Но он, кажется, опирается на ряд более технических компонентов.

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

Расширения VS Code

У VS Code иная концепция и номенклатура подсветки кода (как и многого другого). Примечание: аналогичные концепции использует Language Server Protocol, но их обсуждение выходит за рамки статьи.

Подсветка текста в VS Code называется диагностикой. В нее входит следующее:

  • Diagnostic: содержит информацию о наличии проблемы в документе, серьезности этой проблемы, и прочих данных.
  • DiagnosticCollection: сопоставляет одну или несколько диагностик с файлами рабочего пространства.
  • ExtensionContext: центральный объект расширений VS Code. Он предоставляет «коллекцию утилит, относящихся к расширению» - регистрацию диагностики, автодополнение кода, и т. д.

Идея тут в том, что в ExtensionContext можно зарегистрировать одну или несколько DiagnosticCollections, а затем добавить к ней объекты Diagnostic, зарегистрировав их для определенных файлов текущей рабочей области. Это позволяет отображать коллекции в редакторах. Так как расширения VS Code написаны на TypeScript, код в этой секции тоже написан на этом языке.

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

export function analyseDocument(
doc: vscode.TextDocument,
diagnostics: vscode.DiagnosticCollection
) { ... }

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

Наивный подход – вызов функции при помощи созданного вручную TextDocument и DiagnosticCollection, и последующая валидация, содержит ли DiagnosticCollection правильные объекты Diagnostic. Однако есть способ проще.

Тестирование в копии VS Code

Существует пакет npm (Node.js Package Manager) @vscode/test-electron, при помощи которого, помимо всего прочего, можно заранее настроить рабочее пространство с папками и файлами, открыть копию VS Code из теста внутри этого пространства и использовать находящиеся в нем документы для тестирования.

Так как тест автоматически откроет VS Code с рабочим пространством внутри (благодаря настройке test-electron), наш первый шаг – открытие документа из этого рабочего пространства. Как вариант, можно создать новые документы и на лету наполнить их содержимым, если для ваших тестов это необходимо.

const fileUri = await getFileFromWorkspace("IncompleteVarargs.cs");
const textDocument = await vscode.workspace.openTextDocument(fileUri);
await vscode.window.showTextDocument(textDocument);

Здесь getFileFromWorkspace() – служебный метод, который возвращает URI файла для заданного имени файла (IncompleteVarargs.cs) в рабочем пространстве. IncompleteVarargs.cs содержит вызов метода anObject.setHeaders() с нечетным количеством аргументов, который можно использовать для тестирования.

В плане открытия документа разница между openTextDocument() и showTextDocument() в том, что первый не показывает новую вкладку редактора, а второй это делает, но вам нужны оба этих метода.

Затем можно получить всю диагностику рабочего пространства через API VS Code, и выбрать диагностику для тестируемого файла.

//Массив пар Uri и Diagnostic[]
const diagnostics = vscode.languages.getDiagnostics();
//Отдельная пара Uri и Diagnostic[]
const uriAndDiagnostics = diagnostics[0];
//Типа Diagnostic[]
const diagnosticsForFile = diagnostics[0][1];
//Отдельный объект Diagnostic
const diagnostic = diagnostics[0][1][1];
//Отдельная валидация каждого свойства
assert.strictEqual(diagnostic.severity, DiagnosticSeverity.Error);
assert.strictEqual(diagnostic.message, "An even number of arguments must be passed in.");
assert.strictEqual(diagnostic.range.start.line, 5);
assert.strictEqual(diagnostic.range.start.character, 10);
assert.strictEqual(diagnostic.range.end.line, 5);
assert.strictEqual(diagnostic.range.end.character, 20);
//Или более удобным способом через один объект
assert.deepStrictEqual(diagnostic, {
severity: DiagnosticSeverity.Error,
message: "An even number of arguments must be passed in.",
range: {
start: { line: 5, character: 10 },
end: { line: 5, character: 12 }
}
});

Хорошее ли это решение?

Теперь это очень похоже на решение для Visual Studio. Однако оно может быть сложнее или тяжелее в плане запроса диагностики через множественную индексацию массивов. Однако при использовании глубоких проверок равенства объектов можно сильно улучшить простоту и читабельность теста

В целом могу сказать почти то же самое, что и про пример Visual Studio. Однако тут удобнее то, что можно правильно настроить реальное рабочее пространство VS Code и проводить тестирование внутри него. К тому же у вас есть доступ к другой функциональности VS Code. В результате можно проверить функциональность расширения с точки зрения конечного пользователя.

Есть ли альтернатива?

Собирая материал для статьи, я нашел проект stylelint/vscode-stylelint в GitHub – он использует возможности тестирования моментальных снимков библиотеки Jest. Это здорово подходит для плавного перехода к возможностям следующей IDE. Цитирую с сайта Jest:

«Типичный тест-сценарий для моментального снимка отрисовывает компонент UI, делает снимок, а затем сравнивает его с эталонным снимком, который хранится в тесте. Тест упадет, если два снимка не совпадают: или изменение не запланировано, или нужно обновить эталон под новую версию компонента».

Код теста получает объекты Diagnostic из документа, а затем проводит валидацию снимков.

const diagnostics = await waitForDiagnostics(document);
expect(diagnostics.map(normalizeDiagnostic)).toMatchSnapshot();

Валидация выполняется на основании снимка, в котором хранятся ожидаемые значения свойств объекта Diagnostic, например, так (точный пример для вышеупомянутого снимка):

Object {
"code": "plugin/foo-bar",
"message": "Bar (plugin/foo-bar)",
"range": Object {
"end": Object {
"character": 6,
"line": 0,
},
"start": Object {
"character": 5,
"line": 0,
},
},
"severity": 1,
"source": "Stylelint",
}

Мне куда больше нравится это решение, так как тут:

  • Код теста отделен от тестовых данных;
  • Код тестов гораздо компактнее
  • Проще понять, что и как подсвечивается.

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

Платформа IntelliJ

У IntelliJ свои собственные уникальные концепции подсветки синтаксиса и ошибок. Одна из них называется Inspections и создана для статического анализа кода. Я продемонстрирую ее работу далее.

Интерфейс структуры программы (PSI)

Inspections и многие другие опции платформы используют так называемый PSI (Program Structure Interface) для работы с элементами кода в редакторе.

Цитирую из документации IntelliJ Platform Plugin SDK:

"Файл PSI (Program Structure Interface) – это корень структуры, описывающей содержимое файла как иерархию элементов в определенном языке программирования».

Это можно представить, как абстрактную AST (Abstract Syntax Tree), которая генерируется для каждого файла на основании грамматических определений соответствующего языка. Это похоже на работу с Reflections API в Java, и вы можете запрашивать классы, методы, конструкторы, и т. д. К примеру, этот код Java:

public class PSIDemo {
public void aMethod() {
}
}

даст вот такое дерево PSI:


Inspections

Для внедрения инспекции нужно внедрить класс, расширяющий базовый класс LocalInspectionsTool и один из его методов buildVisitor():

  • Возвращающийся класс visitor соответствует применению паттерна проектирования Visitor и позволяет обращаться к различным типам узлов в деревьях PSI и анализировать их.
  • Соответствующий тип ProblemsHolder позволяет регистрировать элементы PSI, чтобы подсвечивать их, выводить сообщение об ошибке, специально форматировать и, опционально, исправлять на лету.
class IncompleteVarargsInspection extends LocalInspectionTool {
@Override
public PsiElementVisitor buildVisitor(ProblemsHolder holder, boolean isOnTheFly) { ... }
}

Для тестирования мы нацелимся на вышеупомянутый метод buildVisitor() и этот класс инспекции в целом.

Тестирование в редакторе в памяти

Несмотря на то, что платформа IntelliJ поддерживает UI-тестирование фич IDE через своего собственного IntelliJ UI Test Robot, инспекции не тестируются в реальной копии IDE или реальном редакторе. Но для полноценного тестирования инспекций это и не нужно.

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

<warning descr="expected warning message">the code to be highlighted</warning>

Код тестирования исходного примера будет выглядеть примерно так:

@Test
void shouldReportIncompleteParams() {
getFixture().configureByText("IncompleteVarargs.java", """
class IncompleteVarargs {
void aMethod() {
Request request = new Request();
request.<error descr=”An even number of arguments must be passed in.”>setHeaders</error>(“header”, “value”, “another header”);
}
}""");
getFixture().enableInspections(new  IncompleteVarargsInspection());
getFixture().testHighlighting(true, false, true);
}

Тест выполняет следующие шаги:

  • Перед запуском тест-метода он инициализирует пустой проект, основанный на базовом классе платформы IntelliJ, который расширяется тест-классом (для краткости я убрал это из примера).
  • configureByText() загружает файл Java по имени IncompleteVarargs.java в проект и создает редактор в памяти с этим содержимым.
    • Если вы загружаете файл в редактор при помощи комбинации имени файла и текста, а не пути к существующему файлу, вам не придется извлекать тестовые данные в разные файлы, и это может упростить поддержку.
    • Лично я использую смешанный подход: обычно я загружаю небольшие файлы через имя файла и текст, и разделяю содержимое на разные файлы, если они большие, или фича требует, чтобы файл находился в определенной файловой структуре.
    • enableInspections() настраивает внедрение инспекции, для которой тестируется подсветка.
    • testHighlighting() запускает инспекцию и сравнивает результаты подсветки с содержимым, которое мы задали в configureByText(). Аргументы используются для настройки тестируемых уровней безопасности и соответствующих тегов разметки, которые нужно включить в файл тестовых данных. Это можно интерпретировать, как:
testHighlighting(/*checkWarnings*/ true, /*checkInfos*/ false, /*checkWeakWarnings*/ true);

Хорошее ли это решение?

Я думаю, что среди трех IDE-решений это, находящееся на уровне интеграционного тестирования, лучше всего подходит для тестирования подсветки, и лично мне оно больше всего нравится – вот почему:

  • Тест довольно однозначно настраивается – как в этом случае, так и во многих других.
  • Тут меньше кода теста и меньше тестовых данных, и необязательно нужно внедрять специальные утилиты – многие из них уже предоставлены платформой IntelliJ. Это упрощает внедрение, поддержку и понимание теста.
  • Информация о подсветке не отделена от тестовых данных, разметка XML встроена в тестовые файлы. Поэтому проще понять, как будет выглядеть подсветка в реальном редакторе, и где она будет применена.

Выбор за вами

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

Возможно, для технологий и техник, упомянутых в статье, существуют решения поновее и поудобнее. Вне зависимости от возможностей каждой IDE или соответствующих утилит, эти примеры хорошо демонстрируют, что определенные уровни тестирования (в этом случае интеграционный / end-to-end) подходят для тестирования определенных решений лучше, чем юнит-тесты. Выбирайте с умом.

Обсудить в форуме