ist jetzt verfügbar! Lesen Sie über die neuen Funktionen und Fehlerbehebungen vom November.

Testing API

Die Testing API ermöglicht es Visual Studio Code-Erweiterungen, Tests im Arbeitsbereich zu entdecken und Ergebnisse zu veröffentlichen. Benutzer können Tests in der Test Explorer-Ansicht, über Dekorationen und innerhalb von Befehlen ausführen. Mit diesen neuen APIs unterstützt Visual Studio Code reichhaltigere Anzeigen von Ausgaben und Unterschieden, als es bisher möglich war.

Hinweis: Die Testing API ist in VS Code Version 1.59 und höher verfügbar.

Beispiele

Es gibt zwei Testanbieter, die vom VS Code-Team gepflegt werden

Tests entdecken

Tests werden vom TestController bereitgestellt, der eine global eindeutige ID und eine menschenlesbare Beschriftung benötigt, um erstellt zu werden.

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

Um Tests zu veröffentlichen, fügen Sie TestItems als Kinder zur items-Sammlung des Controllers hinzu. TestItems sind die Grundlage der Test-API in der TestItem-Schnittstelle und ein generischer Typ, der einen Testfall, eine Suite oder ein Baumansicht-Element beschreiben kann, wie es im Code existiert. Sie können wiederum children haben und so eine Hierarchie bilden. Hier ist zum Beispiel eine vereinfachte Version, wie die Beispiel-Testerweiterung Tests erstellt

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);
  }
  // ...
});

Ähnlich wie bei Diagnosen liegt es größtenteils an der Erweiterung, zu steuern, wann Tests entdeckt werden. Eine einfache Erweiterung könnte den gesamten Arbeitsbereich beobachten und alle Tests in allen Dateien bei der Aktivierung parsen. Das sofortige Parsen aller Dateien kann jedoch bei großen Arbeitsbereichen langsam sein. Stattdessen können Sie zwei Dinge tun

  1. Tests für eine Datei aktiv entdecken, wenn sie im Editor geöffnet wird, indem Sie vscode.workspace.onDidOpenTextDocument beobachten.
  2. item.canResolveChildren = true setzen und den controller.resolveHandler einstellen. Der resolveHandler wird aufgerufen, wenn der Benutzer eine Aktion ausführt, die die Entdeckung von Tests erzwingt, z. B. durch Erweitern eines Elements im Test Explorer.

So könnte diese Strategie in einer Erweiterung aussehen, die Dateien verzögert parst

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

Die Implementierung von discoverAllFilesInWorkspace kann mit der vorhandenen Dateibeobachtungsfunktionalität von VS Code erstellt werden. Wenn der resolveHandler aufgerufen wird, sollten Sie weiterhin auf Änderungen achten, damit die Daten im Test Explorer auf dem neuesten Stand bleiben.

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;
    })
  );
}

Die TestItem-Schnittstelle ist einfach und bietet keinen Platz für benutzerdefinierte Daten. Wenn Sie zusätzliche Informationen mit einem TestItem verknüpfen müssen, können Sie eine WeakMap verwenden.

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);

Es ist garantiert, dass die TestItem-Instanzen, die an alle TestController-bezogenen Methoden übergeben werden, dieselben sind wie die ursprünglich aus createTestItem erstellten, sodass Sie sicher sein können, dass das Abrufen des Elements aus der testData-Map funktioniert.

Für dieses Beispiel speichern wir einfach den Typ jedes Elements

enum ItemType {
  File,
  TestCase
}

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

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

Tests ausführen

Tests werden über TestRunProfiles ausgeführt. Jedes Profil gehört zu einer bestimmten Ausführungsart kind: run, debug oder coverage. Die meisten Test-Erweiterungen haben höchstens ein Profil in jeder dieser Gruppen, aber mehr sind erlaubt. Wenn Ihre Erweiterung beispielsweise Tests auf mehreren Plattformen ausführt, könnten Sie für jede Kombination aus Plattform und kind ein Profil haben. Jedes Profil hat einen runHandler, der aufgerufen wird, wenn ein Lauf dieses Typs angefordert wird.

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);
  }
);

Der runHandler sollte mindestens einmal controller.createTestRun aufrufen und die ursprüngliche Anfrage durchgeben. Die Anfrage enthält die Tests, die in den Testlauf include (die weggelassen wird, wenn der Benutzer alle Tests ausführen wollte) und möglicherweise Tests, die aus dem Lauf exclude sollen. Die Erweiterung sollte das resultierende TestRun-Objekt verwenden, um den Status der an dem Lauf beteiligten Tests zu aktualisieren. Zum Beispiel

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();
}

Zusätzlich zum runHandler können Sie einen configureHandler für das TestRunProfile festlegen. Wenn vorhanden, hat VS Code eine Benutzeroberfläche, die es dem Benutzer ermöglicht, den Testlauf zu konfigurieren, und ruft den Handler auf, wenn dies geschieht. Von hier aus können Sie Dateien öffnen, eine Schnellauswahl anzeigen oder alles tun, was für Ihr Testframework angemessen ist.

VS Code behandelt Testkonfigurationen absichtlich anders als Debug- oder Task-Konfigurationen. Dies sind traditionell Editor- oder IDE-zentrierte Funktionen und werden in speziellen Dateien im .vscode-Ordner konfiguriert. Tests wurden jedoch traditionell von der Kommandozeile ausgeführt, und die meisten Testframeworks haben bestehende Konfigurationsstrategien. Daher vermeiden wir in VS Code eine Duplizierung von Konfigurationen und überlassen diese stattdessen den Erweiterungen.

Testausgabe

Zusätzlich zu den an TestRun.failed oder TestRun.errored übergebenen Nachrichten können Sie generische Ausgaben mit run.appendOutput(str) anhängen. Diese Ausgabe kann in einem Terminal über die **Test: Ausgabe anzeigen** und über verschiedene Schaltflächen in der Benutzeroberfläche, wie das Terminalsymbol in der Test Explorer-Ansicht, angezeigt werden.

Da die Zeichenkette in einem Terminal gerendert wird, können Sie die vollständige Menge an ANSI-Codes verwenden, einschließlich der Stile, die im npm-Paket ansi-styles verfügbar sind. Beachten Sie, dass Zeilen aufgrund der Ausgabe in einem Terminal mit CRLF (\r\n) umbrochen werden müssen, nicht nur mit LF (\n), was möglicherweise die Standardausgabe einiger Tools ist.

Testabdeckung

Die Testabdeckung wird über die Methode run.addCoverage() mit einem TestRun verknüpft. Üblicherweise geschieht dies durch runHandler von Profilen des TestRunProfileKind.Coverage, aber es ist möglich, sie während jedes Testlaufs aufzurufen. Die Methode addCoverage nimmt ein FileCoverage-Objekt entgegen, das eine Zusammenfassung der Abdeckungsdaten in dieser Datei ist.

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 enthält die insgesamt abgedeckte und nicht abgedeckte Anzahl von Anweisungen, Verzweigungen und Deklarationen in jeder Datei. Je nach Laufzeit und Abdeckungsformat können Sie Abdeckung von Anweisungen als Zeilenabdeckung oder Abdeckung von Deklarationen als Funktions- oder Methodenabdeckung bezeichnen. Sie können die Dateitabdeckung für eine einzelne URI mehrmals hinzufügen, wobei die neuen Informationen die alten ersetzen.

Sobald ein Benutzer eine Datei mit Abdeckung öffnet oder eine Datei in der Ansicht **Testabdeckung** erweitert, fordert VS Code weitere Informationen für diese Datei an. Dies geschieht durch Aufrufen einer von der Erweiterung definierten Methode loadDetailedCoverage auf dem TestRunProfile mit dem TestRun, FileCoverage und einem CancellationToken. Beachten Sie, dass die Testlauf- und Dateitabdeckung-Instanzen dieselben sind wie die, die in run.addCoverage verwendet werden, was nützlich ist, um Daten zu verknüpfen. Sie können beispielsweise eine Map von FileCoverage-Objekten zu Ihren eigenen Daten erstellen.

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);
  }
}

Alternativ können Sie FileCoverage mit einer Implementierung unterklassifizieren, die diese Daten enthält.

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 soll ein Promise auf ein Array von DeclarationCoverage- und/oder StatementCoverage-Objekten zurückgeben. Beide Objekte enthalten eine Position oder Range, an der sie in der Quelldatei gefunden werden können. DeclarationCoverage-Objekte enthalten den Namen des deklarierten Elements (z. B. Funktions- oder Methodenname) und die Anzahl der Aufrufe dieser Deklaration. Statements enthalten die Anzahl der Ausführungen sowie null oder mehr zugeordnete Verzweigungen. Weitere Informationen finden Sie in den Typdefinitionen in vscode.d.ts.

In vielen Fällen haben Sie möglicherweise persistente Dateien aus Ihrem Testlauf. Es ist bewährte Praxis, solche Abdeckungsausgaben im temporären Verzeichnis des Systems abzulegen (das Sie über require('os').tmpdir() abrufen können), aber Sie können sie auch eifrig bereinigen, indem Sie auf VS Codes Hinweis lauschen, dass es den Testlauf nicht mehr benötigt.

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 });
  });
}

Test-Tags

Manchmal können Tests nur unter bestimmten Konfigurationen ausgeführt werden oder gar nicht. Für diese Anwendungsfälle können Sie Test-Tags verwenden. TestRunProfiles können optional ein Tag zugeordnet haben, und wenn sie dies tun, können nur Tests mit diesem Tag unter dem Profil ausgeführt werden. Wenn kein geeignetes Profil zum Ausführen, Debuggen oder Sammeln von Abdeckung für einen bestimmten Test vorhanden ist, werden diese Optionen nicht in der Benutzeroberfläche angezeigt.

// 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];
}

Benutzer können auch in der Test Explorer-Benutzeroberfläche nach Tags filtern.

Nur-Veröffentlichungs-Controller

Das Vorhandensein von Laufprofilen ist optional. Ein Controller darf Tests erstellen, createTestRun außerhalb des runHandler aufrufen und den Status von Tests während des Laufs aktualisieren, ohne ein Profil zu haben. Der häufigste Anwendungsfall hierfür sind Controller, die ihre Ergebnisse aus einer externen Quelle laden, z. B. CI oder Zusammenfassungsdateien.

In diesem Fall sollten diese Controller normalerweise das optionale name-Argument an createTestRun und false für das persist-Argument übergeben. Das Übergeben von false weist VS Code an, das Testergebnis nicht beizubehalten, wie es dies für Läufe im Editor tun würde, da diese Ergebnisse extern neu geladen werden können.

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();
});

Migration von der Test Explorer UI

Wenn Sie eine bestehende Erweiterung haben, die die Test Explorer UI verwendet, empfehlen wir die Migration zur nativen Erfahrung für zusätzliche Funktionen und Effizienz. Wir haben ein Repository mit einem Beispiel für die Migration des Test Adapter-Beispiels in seiner Git-Historie zusammengestellt. Sie können jeden Schritt anzeigen, indem Sie den Commit-Namen auswählen, beginnend mit [1] Create a native TestController.

Zusammenfassend lässt sich sagen, dass die allgemeinen Schritte wie folgt sind

  1. Anstatt einen TestAdapter von der Test Explorer UI's TestHub abzurufen und zu registrieren, rufen Sie const controller = vscode.tests.createTestController(...) auf.

  2. Anstatt testAdapter.tests auszulösen, wenn Sie Tests entdecken oder neu entdecken, erstellen und fügen Sie stattdessen Tests zu controller.items hinzu, z. B. durch Aufrufen von controller.items.replace mit einem Array von entdeckten Tests, die durch Aufrufen von vscode.test.createTestItem erstellt wurden. Beachten Sie, dass Sie beim Ändern von Tests Eigenschaften des Testelements ändern und deren Kinder aktualisieren können, und Änderungen werden automatisch in der VS Code-Benutzeroberfläche reflektiert.

  3. Um Tests anfänglich zu laden, anstatt auf den Aufruf der Methode testAdapter.load() zu warten, setzen Sie controller.resolveHandler = () => { /* Tests entdecken */ }. Weitere Informationen dazu, wie die Testentdeckung funktioniert, finden Sie unter Tests entdecken.

  4. Um Tests auszuführen, sollten Sie ein Run Profile mit einer Handler-Funktion erstellen, die const run = controller.createTestRun(request) aufruft. Anstatt ein testStates-Ereignis auszulösen, übergeben Sie TestItems an Methoden des run, um deren Status zu aktualisieren.

Zusätzliche Beitrags-Punkte

Der Beitrags-Punkt für Menüs testing/item/context menü kann verwendet werden, um Menüpunkte zu Tests in der Test Explorer-Ansicht hinzuzufügen. Platzieren Sie Menüpunkte in der Gruppe inline, um sie inline anzuzeigen. Alle anderen Menüpunktgruppen werden in einem Kontextmenü angezeigt, auf das Sie mit der rechten Maustaste zugreifen können.

Zusätzliche Kontextschlüssel sind in den when-Klauseln Ihrer Menüpunkte verfügbar: testId, controllerId und testItemHasUri. Für komplexere when-Szenarien, bei denen Aktionen optional für verschiedene Testelemente verfügbar sein sollen, sollten Sie den in-Operator in Betracht ziehen.

Wenn Sie einen Test im Explorer anzeigen möchten, können Sie den Test an den Befehl vscode.commands.executeCommand('vscode.revealTestInExplorer', testItem) übergeben.

© . This site is unofficial and not affiliated with Microsoft.