參加你附近的 ,瞭解 VS Code 中的 AI 輔助開發。

測試 API

測試 API 允許 Visual Studio Code 擴充套件發現工作區中的測試併發布結果。使用者可以在測試資源管理器檢視、裝飾和命令中執行測試。透過這些新的 API,Visual Studio Code 支援比以往更豐富的輸出和差異顯示。

注意:測試 API 在 VS Code 1.59 及更高版本中可用。

示例

VS Code 團隊維護著兩個測試提供程式

發現測試

測試由 TestController 提供,它需要一個全域性唯一的 ID 和人類可讀的標籤來建立

const controller = vscode.tests.createTestController(
  'helloWorldTests',
  'Hello World Tests'
);

要釋出測試,您可以將 TestItems 作為子項新增到控制器的 items 集合中。TestItems 是 TestItem 介面中測試 API 的基礎,它們是一種通用型別,可以描述程式碼中存在的測試用例、套件或樹項。它們反過來可以有自己的 children,形成一個層次結構。例如,這裡是示例測試擴充套件如何建立測試的簡化版本

parseMarkdown(content, {
  onTest: (range, numberA, mathOperator, numberB, expectedValue) => {
    // If this is a top-level test, add it to its parent's children. If not,
    // add it to the controller's top level items.
    const collection = parent ? parent.children : controller.items;
    // Create a new ID that's unique among the parent's children:
    const id = [numberA, mathOperator, numberB, expectedValue].join('  ');

    // Finally, create the test item:
    const test = controller.createTestItem(id, data.getLabel(), item.uri);
    test.range = range;
    collection.add(test);
  }
  // ...
});

與診斷類似,何時發現測試主要由擴充套件程式控制。一個簡單的擴充套件程式可能會在啟用時監視整個工作區並解析所有檔案中的所有測試。但是,對於大型工作區,立即解析所有內容可能會很慢。相反,您可以做兩件事

  1. 透過監視 vscode.workspace.onDidOpenTextDocument,在檔案在編輯器中開啟時主動發現該檔案的測試。
  2. 設定 item.canResolveChildren = true 並設定 controller.resolveHandler。如果使用者採取操作要求發現測試,例如透過在測試資源管理器中展開一個項,則會呼叫 resolveHandler

以下是此策略在惰性解析檔案的擴充套件程式中可能的樣子

// First, create the `resolveHandler`. This may initially be called with
// "undefined" to ask for all tests in the workspace to be discovered, usually
// when the user opens the Test Explorer for the first time.
controller.resolveHandler = async test => {
  if (!test) {
    await discoverAllFilesInWorkspace();
  } else {
    await parseTestsInFileContents(test);
  }
};

// When text documents are open, parse tests in them.
vscode.workspace.onDidOpenTextDocument(parseTestsInDocument);
// We could also listen to document changes to re-parse unsaved changes:
vscode.workspace.onDidChangeTextDocument(e => parseTestsInDocument(e.document));

// In this function, we'll get the file TestItem if we've already found it,
// otherwise we'll create it with `canResolveChildren = true` to indicate it
// can be passed to the `controller.resolveHandler` to gets its children.
function getOrCreateFile(uri: vscode.Uri) {
  const existing = controller.items.get(uri.toString());
  if (existing) {
    return existing;
  }

  const file = controller.createTestItem(uri.toString(), uri.path.split('/').pop()!, uri);
  file.canResolveChildren = true;
  return file;
}

function parseTestsInDocument(e: vscode.TextDocument) {
  if (e.uri.scheme === 'file' && e.uri.path.endsWith('.md')) {
    parseTestsInFileContents(getOrCreateFile(e.uri), e.getText());
  }
}

async function parseTestsInFileContents(file: vscode.TestItem, contents?: string) {
  // If a document is open, VS Code already knows its contents. If this is being
  // called from the resolveHandler when a document isn't open, we'll need to
  // read them from disk ourselves.
  if (contents === undefined) {
    const rawContent = await vscode.workspace.fs.readFile(file.uri);
    contents = new TextDecoder().decode(rawContent);
  }

  // some custom logic to fill in test.children from the contents...
}

discoverAllFilesInWorkspace 的實現可以使用 VS Code 現有的檔案監視功能構建。當呼叫 resolveHandler 時,您應該繼續監視更改,以便測試資源管理器中的資料保持最新。

async function discoverAllFilesInWorkspace() {
  if (!vscode.workspace.workspaceFolders) {
    return []; // handle the case of no open folders
  }

  return Promise.all(
    vscode.workspace.workspaceFolders.map(async workspaceFolder => {
      const pattern = new vscode.RelativePattern(workspaceFolder, '**/*.md');
      const watcher = vscode.workspace.createFileSystemWatcher(pattern);

      // When files are created, make sure there's a corresponding "file" node in the tree
      watcher.onDidCreate(uri => getOrCreateFile(uri));
      // When files change, re-parse them. Note that you could optimize this so
      // that you only re-parse children that have been resolved in the past.
      watcher.onDidChange(uri => parseTestsInFileContents(getOrCreateFile(uri)));
      // And, finally, delete TestItems for removed files. This is simple, since
      // we use the URI as the TestItem's ID.
      watcher.onDidDelete(uri => controller.items.delete(uri.toString()));

      for (const file of await vscode.workspace.findFiles(pattern)) {
        getOrCreateFile(file);
      }

      return watcher;
    })
  );
}

TestItem 介面很簡單,沒有空間放置自定義資料。如果您需要將額外資訊與 TestItem 關聯,您可以使用 WeakMap

const testData = new WeakMap<vscode.TestItem, MyCustomData>();

// to associate data:
const item = controller.createTestItem(id, label);
testData.set(item, new MyCustomData());

// to get it back later:
const myData = testData.get(item);

保證傳遞給所有 TestController 相關方法的 TestItem 例項將與最初從 createTestItem 建立的例項相同,因此您可以確保從 testData 對映中獲取項將起作用。

對於此示例,我們只儲存每個專案的型別

enum ItemType {
  File,
  TestCase
}

const testData = new WeakMap<vscode.TestItem, ItemType>();

const getType = (testItem: vscode.TestItem) => testData.get(testItem)!;

執行測試

測試透過 TestRunProfile 執行。每個配置檔案都屬於特定的執行 kind:執行、除錯或覆蓋。大多數測試擴充套件在每個組中最多有一個配置檔案,但允許更多。例如,如果您的擴充套件在多個平臺上執行測試,您可以為平臺和 kind 的每個組合設定一個配置檔案。每個配置檔案都有一個 runHandler,當請求執行該型別的測試時會呼叫它。

function runHandler(
  shouldDebug: boolean,
  request: vscode.TestRunRequest,
  token: vscode.CancellationToken
) {
  // todo
}

const runProfile = controller.createRunProfile(
  'Run',
  vscode.TestRunProfileKind.Run,
  (request, token) => {
    runHandler(false, request, token);
  }
);

const debugProfile = controller.createRunProfile(
  'Debug',
  vscode.TestRunProfileKind.Debug,
  (request, token) => {
    runHandler(true, request, token);
  }
);

runHandler 應該至少呼叫一次 controller.createTestRun,並傳遞原始請求。請求包含要在測試執行中 include 的測試(如果使用者要求執行所有測試則省略)以及可能要從執行中 exclude 的測試。擴充套件程式應使用生成的 TestRun 物件更新執行中涉及的測試狀態。例如

async function runHandler(
  shouldDebug: boolean,
  request: vscode.TestRunRequest,
  token: vscode.CancellationToken
) {
  const run = controller.createTestRun(request);
  const queue: vscode.TestItem[] = [];

  // Loop through all included tests, or all known tests, and add them to our queue
  if (request.include) {
    request.include.forEach(test => queue.push(test));
  } else {
    controller.items.forEach(test => queue.push(test));
  }

  // For every test that was queued, try to run it. Call run.passed() or run.failed().
  // The `TestMessage` can contain extra information, like a failing location or
  // a diff output. But here we'll just give it a textual message.
  while (queue.length > 0 && !token.isCancellationRequested) {
    const test = queue.pop()!;

    // Skip tests the user asked to exclude
    if (request.exclude?.includes(test)) {
      continue;
    }

    switch (getType(test)) {
      case ItemType.File:
        // If we're running a file and don't know what it contains yet, parse it now
        if (test.children.size === 0) {
          await parseTestsInFileContents(test);
        }
        break;
      case ItemType.TestCase:
        // Otherwise, just run the test case. Note that we don't need to manually
        // set the state of parent tests; they'll be set automatically.
        const start = Date.now();
        try {
          await assertTestPasses(test);
          run.passed(test, Date.now() - start);
        } catch (e) {
          run.failed(test, new vscode.TestMessage(e.message), Date.now() - start);
        }
        break;
    }

    test.children.forEach(test => queue.push(test));
  }

  // Make sure to end the run after all tests have been executed:
  run.end();
}

除了 runHandler 之外,您還可以在 TestRunProfile 上設定 configureHandler。如果存在,VS Code 將提供 UI 以允許使用者配置測試執行,並在他們這樣做時呼叫處理程式。從這裡,您可以開啟檔案、顯示快速選擇或執行適合您的測試框架的任何操作。

VS Code 有意以不同於除錯或任務配置的方式處理測試配置。這些傳統上是以編輯器或 IDE 為中心的功能,並在 .vscode 資料夾中的特殊檔案中配置。但是,測試傳統上是從命令列執行的,並且大多數測試框架都有現有的配置策略。因此,在 VS Code 中,我們避免配置重複,而是將其留給擴充套件程式處理。

測試輸出

除了傳遞給 TestRun.failedTestRun.errored 的訊息之外,您還可以使用 run.appendOutput(str) 附加通用輸出。此輸出可以使用測試:顯示輸出在終端中顯示,並透過 UI 中的各種按鈕(例如測試資源管理器檢視中的終端圖示)顯示。

由於字串在終端中呈現,您可以使用全套 ANSI 程式碼,包括 ansi-styles npm 包中可用的樣式。請記住,由於它在終端中,行必須使用 CRLF (\r\n) 換行,而不僅僅是 LF (\n),這可能是某些工具的預設輸出。

測試覆蓋率

測試覆蓋率透過 run.addCoverage() 方法與 TestRun 關聯。通常這應該由 TestRunProfileKind.Coverage 配置檔案的 runHandler 完成,但可以在任何測試執行期間呼叫它。addCoverage 方法接受一個 FileCoverage 物件,它是該檔案中覆蓋率資料的摘要

async function runHandler(
  shouldDebug: boolean,
  request: vscode.TestRunRequest,
  token: vscode.CancellationToken
) {
  // ...

  for await (const file of readCoverageOutput()) {
    run.addCoverage(new vscode.FileCoverage(file.uri, file.statementCoverage));
  }
}

FileCoverage 包含每個檔案中語句、分支和宣告的總覆蓋和未覆蓋計數。根據您的執行時和覆蓋率格式,您可能會看到語句覆蓋率稱為行覆蓋率,或宣告覆蓋率稱為函式或方法覆蓋率。您可以多次為單個 URI 新增檔案覆蓋率,在這種情況下,新資訊將替換舊資訊。

一旦使用者開啟帶有覆蓋率的檔案或在測試覆蓋率檢視中展開檔案,VS Code 會請求該檔案的更多資訊。它透過呼叫 TestRunProfile 上定義的 loadDetailedCoverage 方法來完成,並附帶 TestRunFileCoverageCancellationToken。請注意,測試執行和檔案覆蓋率例項與 run.addCoverage 中使用的例項相同,這對於關聯資料很有用。例如,您可以建立 FileCoverage 物件到您自己資料的對映

const coverageData = new WeakMap<vscode.FileCoverage, MyCoverageDetails>();

profile.loadDetailedCoverage = (testRun, fileCoverage, token) => {
  return coverageData.get(fileCoverage).load(token);
};

async function runHandler(
  shouldDebug: boolean,
  request: vscode.TestRunRequest,
  token: vscode.CancellationToken
) {
  // ...

  for await (const file of readCoverageOutput()) {
    const coverage = new vscode.FileCoverage(file.uri, file.statementCoverage);
    coverageData.set(coverage, file);
    run.addCoverage(coverage);
  }
}

或者,您可以將 FileCoverage 子類化,並實現包含該資料

class MyFileCoverage extends vscode.FileCoverage {
  // ...
}

profile.loadDetailedCoverage = async (testRun, fileCoverage, token) => {
  return fileCoverage instanceof MyFileCoverage ? await fileCoverage.load() : [];
};

async function runHandler(
  shouldDebug: boolean,
  request: vscode.TestRunRequest,
  token: vscode.CancellationToken
) {
  // ...

  for await (const file of readCoverageOutput()) {
    // 'file' is MyFileCoverage:
    run.addCoverage(file);
  }
}

loadDetailedCoverage 預計會返回一個 Promise,其值為 DeclarationCoverage 和/或 StatementCoverage 物件陣列。這兩個物件都包含一個 PositionRange,可以在原始檔中找到它們。DeclarationCoverage 物件包含所宣告事物的名稱(例如函式或方法名稱)以及該宣告被進入或呼叫的次數。語句包含它們被執行的次數,以及零個或多個相關分支。有關更多資訊,請參閱 vscode.d.ts 中的型別定義。

在許多情況下,您可能會在測試執行中留下持久檔案。最佳實踐是將此類覆蓋率輸出放在系統的臨時目錄中(您可以透過 require('os').tmpdir() 檢索),但您也可以透過偵聽 VS Code 的提示來主動清理它們,表明它不再需要保留測試執行

import { promises as fs } from 'fs';

async function runHandler(
  shouldDebug: boolean,
  request: vscode.TestRunRequest,
  token: vscode.CancellationToken
) {
  // ...

  run.onDidDispose(async () => {
    await fs.rm(coverageOutputDirectory, { recursive: true, force: true });
  });
}

測試標籤

有時測試只能在某些配置下執行,或者根本不能執行。對於這些用例,您可以使用測試標籤。TestRunProfiles 可以選擇關聯一個標籤,如果它們有標籤,則只有具有該標籤的測試才能在該配置檔案下執行。同樣,如果沒有合格的配置檔案來執行、除錯或收集特定測試的覆蓋率,則這些選項將不會顯示在 UI 中。

// Create a new tag with an ID of "runnable"
const runnableTag = new TestTag('runnable');

// Assign it to a profile. Now this profile can only execute tests with that tag.
runProfile.tag = runnableTag;

// Add the "runnable" tag to all applicable tests.
for (const test of getAllRunnableTests()) {
  test.tags = [...test.tags, runnableTag];
}

使用者還可以在測試資源管理器 UI 中按標籤過濾。

僅釋出控制器

執行配置檔案的存在是可選的。控制器可以在 runHandler 之外建立測試、呼叫 createTestRun 並更新執行中的測試狀態,而無需配置檔案。這種情況的常見用例是控制器從外部源(例如 CI 或摘要檔案)載入其結果。

在這種情況下,這些控制器通常應該將可選的 name 引數傳遞給 createTestRun,並將 persist 引數設定為 false。在此處傳遞 false 會指示 VS Code 不要保留測試結果,就像它在編輯器中執行一樣,因為這些結果可以從外部源重新載入。

const controller = vscode.tests.createTestController(
  'myCoverageFileTests',
  'Coverage File Tests'
);

vscode.commands.registerCommand('myExtension.loadTestResultFile', async file => {
  const info = await readFile(file);

  // set the controller items to those read from the file:
  controller.items.replace(readTestsFromInfo(info));

  // create your own custom test run, then you can immediately set the state of
  // items in the run and end it to publish results:
  const run = controller.createTestRun(
    new vscode.TestRunRequest(),
    path.basename(file),
    false
  );
  for (const result of info) {
    if (result.passed) {
      run.passed(result.item);
    } else {
      run.failed(result.item, new vscode.TestMessage(result.message));
    }
  }
  run.end();
});

從測試資源管理器 UI 遷移

如果您有一個使用測試資源管理器 UI 的現有擴充套件,我們建議您遷移到原生體驗以獲得附加功能和效率。我們已經整理了一個儲存庫,其中包含其 Git 歷史記錄中測試介面卡示例的遷移示例。您可以透過選擇提交名稱(從 [1] Create a native TestController 開始)檢視每個步驟。

總而言之,一般步驟是

  1. 不是從測試資源管理器 UI 的 TestHub 檢索和註冊 TestAdapter,而是呼叫 const controller = vscode.tests.createTestController(...)

  2. 發現或重新發現測試時,不是觸發 testAdapter.tests,而是建立測試並將其推送到 controller.items,例如透過呼叫 controller.items.replace 並傳入透過呼叫 vscode.test.createTestItem 建立的發現的測試陣列。請注意,隨著測試的更改,您可以更改測試項上的屬性並更新其子項,更改將自動反映在 VS Code 的 UI 中。

  3. 要最初載入測試,不是等待 testAdapter.load() 方法呼叫,而是設定 controller.resolveHandler = () => { /* discover tests */ }。有關測試發現如何工作的更多資訊,請參閱發現測試

  4. 要執行測試,您應該建立一個具有處理函式的執行配置檔案,該函式呼叫 const run = controller.createTestRun(request)。不是觸發 testStates 事件,而是將 TestItems 傳遞給 run 上的方法以更新其狀態。

附加貢獻點

testing/item/context 選單貢獻點可用於將選單項新增到測試資源管理器檢視中的測試。將選單項放置在 inline 組中以使其內聯。所有其他選單項組將顯示在可透過滑鼠右鍵訪問的上下文選單中。

在選單項的 when 子句中可以使用附加的上下文鍵testIdcontrollerIdtestItemHasUri。對於更復雜的 when 場景,如果您希望操作可選擇地用於不同的測試項,請考慮使用 in 條件運算子

如果您想在資源管理器中顯示測試,可以將測試傳遞給命令 vscode.commands.executeCommand('vscode.revealTestInExplorer', testItem)