이 출시되었습니다! 11월의 새로운 기능 및 수정 사항을 읽어보세요.

테스트 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'
);

테스트를 게시하려면 TestItem을 컨트롤러의 items 컬렉션에 자식으로 추가합니다. TestItemTestItem 인터페이스의 테스트 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);

createTestItem에서 원래 생성된 TestItem 인스턴스는 모든 TestController 관련 메서드에 전달되는 TestItem 인스턴스와 동일하게 보장되므로 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 외에도 TestRunProfileconfigureHandler를 설정할 수 있습니다. 존재하는 경우 VS Code는 사용자가 테스트 실행을 구성할 수 있도록 UI를 제공하고 사용자가 그렇게 할 때 핸들러를 호출합니다. 여기서 파일을 열거나, 빠른 선택을 표시하거나, 테스트 프레임워크에 적합한 작업을 수행할 수 있습니다.

VS Code는 테스트 구성을 디버그 또는 작업 구성과 다르게 의도적으로 처리합니다. 이러한 기능은 전통적으로 편집기 또는 IDE 중심 기능이며 .vscode 폴더의 특수 파일에 구성됩니다. 그러나 테스트는 전통적으로 명령줄에서 실행되었으며 대부분의 테스트 프레임워크에는 기존 구성 전략이 있습니다. 따라서 VS Code에서는 구성 중복을 피하고 대신 확장에서 처리하도록 합니다.

테스트 출력

TestRun.failed 또는 TestRun.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에서 TestRun, FileCoverageCancellationToken과 함께 확장 프로그램에서 정의한 loadDetailedCoverage 메서드를 호출하여 수행됩니다. 테스트 실행 및 파일 커버리지 인스턴스는 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);
  }
}

loadDetailedCoverageDeclarationCoverage 및/또는 StatementCoverage 개체의 배열에 대한 프라미스를 반환해야 합니다. 두 개체 모두 소스 파일에서 찾을 수 있는 Position 또는 Range를 포함합니다. DeclarationCoverage 개체는 선언되는 것(함수 또는 메서드 이름 등)의 이름과 해당 선언이 입력되거나 호출된 횟수를 포함합니다. 문에는 실행된 횟수와 0개 이상의 관련 분기가 포함됩니다. 자세한 내용은 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 });
  });
}

테스트 태그

때때로 테스트는 특정 구성에서만 실행될 수 있거나 전혀 실행되지 않을 수 있습니다. 이러한 경우 테스트 태그를 사용할 수 있습니다. TestRunProfile에는 선택적으로 태그를 연결할 수 있으며, 태그가 있는 경우 해당 태그가 있는 테스트만 프로필에서 실행할 수 있습니다. 마찬가지로 특정 테스트에서 실행, 디버그 또는 커버리지 수집에 적합한 프로필이 없으면 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 또는 요약 파일과 같은 외부 소스에서 결과를 로드하는 컨트롤러입니다.

이 경우 이러한 컨트롤러는 일반적으로 createTestRun에 선택적 name 인수를 전달하고 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에서 마이그레이션

기존에 Test Explorer UI를 사용하는 확장이 있는 경우 추가 기능과 효율성을 위해 네이티브 환경으로 마이그레이션하는 것이 좋습니다. Test Adapter 샘플의 예시 마이그레이션이 포함된 리포지토리를 Git 기록에서 준비했습니다. [1] Create a native TestController부터 시작하여 커밋 이름을 선택하여 각 단계를 볼 수 있습니다.

요약하면, 일반적인 단계는 다음과 같습니다.

  1. Test Explorer UI의 TestHubTestAdapter를 검색하고 등록하는 대신 const controller = vscode.tests.createTestController(...)를 호출합니다.

  2. 테스트를 검색하거나 다시 검색할 때 testAdapter.tests를 발급하는 대신, vscode.test.createTestItem을 호출하여 생성된 검색된 테스트 배열로 controller.items.replace를 호출하는 등의 방법으로 테스트를 생성하여 controller.items에 푸시합니다. 테스트가 변경됨에 따라 테스트 항목의 속성을 변경하고 자식을 업데이트할 수 있으며, 변경 사항은 VS Code UI에 자동으로 반영됩니다.

  3. 처음 테스트를 로드하려면 testAdapter.load() 메서드 호출을 기다리는 대신 controller.resolveHandler = () => { /* discover tests */ }를 설정합니다. 테스트 검색이 작동하는 방식에 대한 자세한 내용은 테스트 검색을 참조하십시오.

  4. 테스트를 실행하려면 핸들러 함수가 const run = controller.createTestRun(request)를 호출하는 Run Profile을 만들어야 합니다. testStates 이벤트를 발급하는 대신, run의 메서드에 TestItem을 전달하여 상태를 업데이트합니다.

추가 기여 지점

testing/item/context 메뉴 기여 지점을 사용하여 테스트 탐색기 보기의 테스트에 메뉴 항목을 추가할 수 있습니다. 메뉴 항목을 inline 그룹에 배치하면 인라인으로 표시됩니다. 다른 모든 메뉴 항목 그룹은 마우스 오른쪽 클릭으로 액세스할 수 있는 컨텍스트 메뉴에 표시됩니다.

메뉴 항목의 when 절에서 추가 컨텍스트 키를 사용할 수 있습니다: testId, controllerId, testItemHasUri. 다른 테스트 항목에 대해 선택적으로 사용 가능한 동작을 원하고 더 복잡한 when 시나리오의 경우 in 조건부 연산자 사용을 고려하십시오.

탐색기에서 테스트를 표시하려면 테스트를 vscode.commands.executeCommand('vscode.revealTestInExplorer', testItem) 명령에 전달할 수 있습니다.

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