Другое

Исправление ошибок IIS Express в CI с покрытием кода

Решения для устранения неудачных интеграционных тестов IIS Express в CI при включенном покрытии. Ошибки COM, отсутствие Shim DLL и альтернативные стратегии покрытия для .NET Framework.

Как решить проблемы с интеграционными тестами IIS Express в CI‑pipeline, когда включено покрытие кода для приложений .NET Framework?

Мои интеграционные тесты .NET работают локально, но в CI падают, когда включено покрытие кода. Тесты запускают несколько экземпляров IIS Express (ASP.NET, WebAPI, WCF), которые выполняют веб‑запросы к тестируемым приложениям.

В CI IIS Express не запускается и выдаёт ошибки вида:

[FileNotFoundException: Could not load file or assembly 'Microsoft.VisualStudio.CodeCoverage.Shim, Version=15.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a']

После добавления отсутствующего shim‑DLL я получаю ошибки, связанные с COM:

[COMException (0x80040154): Retrieving the COM class factory for component with CLSID {A9E69610-B80D-11D0-B9B9-00A0C922E750} failed due to the following error: 80040154 Class not registered]

Я пробовал установить CollectAspDotNet в false в файле runsettings, но безуспешно. Мой текущий подход – установить IIS Express в CI, но это выглядит проблематичным из‑за зависимостей от COM‑компонентов.

Какое правильное решение, чтобы запускать интеграционные тесты .NET Framework с покрытием кода в CI без использования COM‑компонентов IIS Express?

IIS Express интеграционные тесты не проходят в CI‑конвейерах, когда включён сбор покрытия кода, главным образом из‑за отсутствующих зависимостей и проблем с регистрацией COM‑компонентов, которые автоматически решаются в локальной среде разработки, но не в CI‑системах. Основная проблема заключается в отсутствии сборки Microsoft.VisualStudio.CodeCoverage.Shim в CI‑окружениях, а также в зависимости IIS Express от COM‑компонентов, которые не зарегистрированы в большинстве CI‑систем.

Содержание

Понимание корневых причин

Ошибка, которую вы наблюдаете, связана с несколькими взаимосвязанными проблемами:

Зависимость от Code Coverage Shim: При включении покрытия кода через Visual Studio Test Platform (vstest.console.exe) запускается агент профилирования, которому требуется сборка Microsoft.VisualStudio.CodeCoverage.Shim.dll. Эта DLL содержит методы выполнения, необходимые для сбора данных о покрытии, но, как подтверждают issues Microsoft на GitHub, эта сборка не развертывается автоматически в дочерних процессах в CI‑окружениях.

Регистрация COM‑компонентов: IIS Express использует COM‑компоненты для работы, особенно для хостинга веб‑приложений. Ошибка CLSID {A9E69610-B80D-11D0-B9B9-00A0C922E750} указывает на то, что требуемый COM‑класс не зарегистрирован. Это происходит, потому что CI‑окружения обычно не регистрируют эти компоненты автоматически.

Проброс переменных среды: Исследования из репозитория vstest Microsoft показывают, что при включении покрытия кода переменные среды, включающие профилирование, передаются дочерним процессам, но Shim DLL не доступна для них, что приводит к FileNotFoundException.

Немедленные решения для отсутствующего Shim DLL

Ручной развертывание Shim DLL

Самый быстрый способ – вручную развернуть Shim DLL в CI‑конвейер:

  1. Найдите Shim DLL: Найдите Microsoft.VisualStudio.CodeCoverage.Shim.dll в установке Visual Studio, обычно в:

    C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\Common7\IDE\Extensions\Microsoft\CodeCoverage\15.0.0\Tools
    
  2. Разверните в CI: Добавьте DLL в каталог выполнения тестов CI или убедитесь, что она находится в PATH:

    yaml
    # Пример Azure DevOps
    - task: UseDotNet@2
      inputs:
        version: '5.0.x'
        includeVersion: 'true'
        performMultiLevelLookup: 'true'
        packageType: 'sdk'
    
    - script: |
        mkdir -p tools
        cp "C:/Program Files (x86)/Microsoft Visual Studio/2019/Enterprise/Common7/IDE/Extensions/Microsoft/CodeCoverage/15.0.0/Tools/Microsoft.VisualStudio.CodeCoverage.Shim.dll" tools/
      displayName: 'Copy Shim DLL to CI'
    

Установка переменных среды

Настройте необходимые переменные среды, чтобы тестовый раннер мог найти Shim DLL:

yaml
- script: |
    set CODE_COVERAGE_PATH=C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\Common7\IDE\Extensions\Microsoft\CodeCoverage\15.0.0\Tools
    set PATH=%CODE_COVERAGE_PATH%;%PATH%
  displayName: 'Set Code Coverage Environment Variables'

Устранение проблем с регистрацией COM

Поскольку COM‑компоненты IIS Express не зарегистрированы в CI‑окружениях, у вас есть несколько вариантов:

Вариант 1: Использовать компоненты Windows Authentication

Регистрируйте необходимые COM‑компоненты вручную в CI‑конвейере:

powershell
# Запустите в скрипте CI для регистрации COM‑компонентов IIS Express
regsvr32 /s "C:\Program Files (x86)\IIS Express\iisexpress.exe"

Вариант 2: Использовать Kestrel вместо IIS Express

Для приложений .NET Core рассмотрите миграцию на хостинг Kestrel:

csharp
// В конфигурации теста используйте Kestrel вместо IIS Express
[AssemblyInitialize]
public static void Initialize(TestContext context)
{
    var builder = new WebHostBuilder()
        .UseStartup<Startup>()
        .UseKestrel()
        .UseUrls("http://localhost:5000");
    
    var host = builder.Build();
    host.Start();
}

Вариант 3: Использовать IIS вместо IIS Express

Если ваш CI‑окружение поддерживает полноценный IIS:

yaml
- task: IISWebAppDeploymentOnMachineGroup@0
  inputs:
    webSiteName: 'Default Web Site'
    package: $(Build.ArtifactStagingDirectory)/drop/*.zip
  displayName: 'Deploy to IIS'

Альтернативные стратегии покрытия

Использовать OpenCover вместо встроенного покрытия

OpenCover – инструмент покрытия кода .NET, который лучше работает с IIS Express в CI‑окружениях:

xml
<!-- Пример OpenCover -->
<Target Name="RunTests">
  <Exec Command="OpenCover.Console.exe -register:user -target:vstest.console.exe -targetargs:&quot;IntegrationTests.dll /EnableCodeCoverage&quot; -output:coverage.xml" />
</Target>

Использовать Coverlet для .NET Core приложений

Для проектов .NET Core Coverlet обеспечивает отличное покрытие без зависимостей от IIS:

xml
<ItemGroup>
  <PackageReference Include="coverlet.collector" Version="6.0.0">
    <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    <PrivateAssets>all</PrivateAssets>
  </PackageReference>
</ItemGroup>

Используйте следующий runsettings:

xml
<RunSettings>
  <DataCollectionRunSettings>
    <DataCollectors>
      <DataCollector friendlyName="Coverlet" 
                    codeCoverageAssemblyPath="coverlet.core.dll">
        <Configuration>
          <Format>opencover</Format>
          <Include>[YourAssemblyName]*</Include>
        </Configuration>
      </DataCollector>
    </DataCollectors>
  </DataCollectionRunSettings>
</RunSettings>

Лучшие практики конфигурации RunSettings

Оптимизированный RunSettings для CI

Создайте специализированный файл runsettings для CI‑окружений:

xml
<?xml version="1.0" encoding="utf-8"?>
<RunSettings>
  <DataCollectionRunSettings>
    <DataCollectors>
      <DataCollector friendlyName="Code Coverage" 
                   uri="datacollector://Microsoft/CodeCoverage/2.0"
                   assemblyQualifiedName="Microsoft.VisualStudio.Coverage.DynamicCoverageDataCollector, Microsoft.VisualStudio.TraceCollector, Version=11.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
        <Configuration>
          <CodeCoverage>
            <!-- Использовать проверяемую инструментацию для лучшей совместимости -->
            <UseVerifiableInstrumentation>true</UseVerifiableInstrumentation>
            <!-- Позволить процессы с низким уровнем целостности (часто в CI) -->
            <AllowLowIntegrityProcesses>true</AllowLowIntegrityProcesses>
            <!-- Собирать данные из дочерних процессов, критичных для IIS Express -->
            <CollectFromChildProcesses>true</CollectFromChildProcesses>
            <!-- При необходимости отключить сбор ASP.NET -->
            <CollectAspDotNet>false</CollectAspDotNet>
            <!-- Добавить конкретные модули для покрытия -->
            <ModulePaths>
              <ModulePath cover="true" path=".*YourAssembly\.dll$"/>
            </ModulePaths>
          </CodeCoverage>
        </Configuration>
      </DataCollector>
    </DataCollectors>
  </DataCollectionRunSettings>
  
  <!-- Настройки выполнения тестов для CI -->
  <TestRunParameters>
    <Parameter name="Environment" value="CI" />
  </TestRunParameters>
</RunSettings>

Условная конфигурация

Используйте условные символы компиляции для различий между CI и локальной средой:

csharp
#if CI
    // Упрощенная настройка тестов для CI
    var builder = new WebHostBuilder()
        .UseStartup<Startup>()
        .UseUrls("http://localhost:5000");
#else
    // Полная настройка IIS Express для локальной разработки
    var builder = new WebHostBuilder()
        .UseStartup<Startup>()
        .UseIISIntegration();
#endif

Рекомендации по настройке CI‑окружения

Требования к агенту Windows CI

Убедитесь, что ваш агент CI соответствует следующим требованиям:

  1. Visual Studio Build Tools: Установите Visual Studio Build Tools с компонентами Code Coverage
  2. IIS Express: Установите IIS Express с правильной регистрацией COM
  3. .NET Framework Runtime: Требуемые версии, соответствующие вашему приложению
  4. PowerShell: Для выполнения скриптов регистрации

Пример конвейера Azure DevOps

Ниже приведена полная конфигурация конвейера Azure DevOps:

yaml
trigger:
- main

pool:
  vmImage: 'windows-latest'

variables:
  solution: '**/*.sln'
  buildPlatform: 'x64'
  buildConfiguration: 'Release'

steps:
- task: NuGetToolInstaller@1
  displayName: 'Install NuGet'

- task: NuGetCommand@2
  inputs:
    restoreSolution: '$(solution)'
  displayName: 'NuGet restore'

- task: VSBuild@1
  inputs:
    solution: '$(solution)'
    msbuildArgs: '/p:DeployOnBuild=true /p:WebPublishMethod=Package /p:PackageAsSingleFile=true /p:SkipInvalidConfigurations=true /p:PackageLocation="$(build.artifactStagingDirectory)"'
    platform: '$(buildPlatform)'
    configuration: '$(buildConfiguration)'
  displayName: 'Build solution'

- task: CopyFiles@2
  inputs:
    SourceFolder: '$(ProgramFiles(x86))\Microsoft Visual Studio\2019\Enterprise\Common7\IDE\Extensions\Microsoft\CodeCoverage\15.0.0\Tools'
    Contents: 'Microsoft.VisualStudio.CodeCoverage.Shim.dll'
    TargetFolder: '$(Build.SourcesDirectory)\tools'
  displayName: 'Copy Shim DLL'

- script: |
    regsvr32 /s "C:\Program Files (x86)\IIS Express\iisexpress.exe"
    set CODE_COVERAGE_PATH=$(Build.SourcesDirectory)\tools
    set PATH=$(CODE_COVERAGE_PATH);%PATH%
  displayName: 'Register COM Components and Set Environment'

- task: VSTest@2
  inputs:
    testAssemblyVer2: |
      **\IntegrationTests.dll
    searchFolder: '$(Build.SourcesDirectory)'
    vsTestVersion: 'toolsVersion'
    platform: '$(buildPlatform)'
    configuration: '$(buildConfiguration)'
    codeCoverageEnabled: true
    runSettingsFile: 'codecoverage.runsettings'
    testFiltercriteria: 'TestCategory!=Integration || TestName~*Fast*'
    overrideTestrunParameters: |
      -TestEnvironment="CI"
  displayName: 'Run integration tests with coverage'

Пример GitHub Actions

yaml
name: CI Pipeline

on:
  push:
    branches: [ main ]

jobs:
  build:
    runs-on: windows-latest
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Setup MSBuild
      uses: microsoft/setup-msbuild@v1.1
      
    - name: Install dependencies
      run: nuget restore MySolution.sln
      
    - name: Build solution
      run: msbuild MySolution.sln -p:Configuration=Release
      
    - name: Copy Shim DLL
      run: |
        mkdir -p tools
        copy "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\Common7\IDE\Extensions\Microsoft\CodeCoverage\15.0.0\Tools\Microsoft.VisualStudio.CodeCoverage.Shim.dll" tools/
        
    - name: Register COM and Setup Environment
      run: |
        regsvr32 /s "C:\Program Files (x86)\IIS Express\iisexpress.exe"
        set CODE_COVERAGE_PATH=%CD%\tools
        set PATH=%CODE_COVERAGE_PATH%;%PATH%
        
    - name: Run tests with coverage
      run: vstest.console.exe IntegrationTests.dll /EnableCodeCoverage /logger:trx
      env:
        CODE_COVERAGE_PATH: %CD%\tools

Долгосрочный путь миграции

Рассмотрите переход на .NET Core/5+

Для новых проектов рассмотрите миграцию на .NET Core или .NET 5+, которые имеют лучшую поддержку CI/CD:

csharp
// Подход к интеграционным тестам .NET Core
[Fact]
public async Task WebApiIntegrationTest()
{
    // Используйте TestServer вместо IIS Express
    using var factory = new WebApplicationFactory<Startup>();
    using var client = factory.CreateClient();
    
    var response = await client.GetAsync("/api/values");
    response.EnsureSuccessStatusCode();
    
    var content = await response.Content.ReadAsStringAsync();
    Assert.Contains("Test", content);
}

Контейнеризация ваших тестов

Используйте Docker‑контейнеры для более стабильных тестовых окружений:

dockerfile
FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build
WORKDIR /src
COPY . .
RUN dotnet restore MySolution.sln
RUN dotnet build MySolution.sln --configuration Release

FROM build AS test
WORKDIR /src
COPY --from=build /src .
RUN dotnet test MySolution.sln --configuration Release --collect:"XPlat Code Coverage"

Используйте фреймворки для тестирования API

Рассмотрите замену IIS Express на фреймворки тестирования API, которые не требуют веб‑хостинга:

csharp
// Используйте TestServer для тестов, похожих на unit‑тесты
[Fact]
public async Task TestWebApiEndpoint()
{
    var webHostBuilder = new WebHostBuilder()
        .UseStartup<Startup>()
        .UseUrls("http://localhost:5000");
    
    using var server = new TestServer(webHostBuilder);
    using var client = server.CreateClient();
    
    var response = await client.GetAsync("/api/values");
    Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}

Внедрив эти решения, вы сможете успешно запускать интеграционные тесты IIS Express с покрытием кода в CI‑pipeline, минимизируя зависимость от COM‑компонентов и проблемных зависимостей.

Источники

  1. Stack Overflow – IIS Express integration tests failing in CI with coverage
  2. GitHub – Microsoft vstest Shim dependency issues
  3. GitHub – Microsoft vstest Environment variables and child processes
  4. JetBrains support – dotCover IIS support removal
  5. OpenCover GitHub repository
  6. NCover support – IIS testing considerations
  7. CodeMaze – .NET Code Coverage guide

Заключение

Чтобы решить проблемы с IIS Express интеграционными тестами в CI‑pipeline при включённом покрытии кода для приложений .NET Framework:

  1. Разверните Shim DLL вручную в CI‑окружение и установите правильные переменные среды, чтобы тестовый раннер мог её найти.
  2. Регистрация COM‑компонентов, необходимых IIS Express, либо рассмотрите альтернативные решения хостинга, такие как Kestrel или полноценный IIS.
  3. Используйте оптимизированные runsettings с AllowLowIntegrityProcesses и CollectFromChildProcesses для лучшей совместимости с CI.
  4. Рассмотрите альтернативные инструменты покрытия, такие как OpenCover или Coverlet, которые работают лучше с IIS Express в CI‑окружениях.
  5. Планируйте миграцию на .NET Core/5+ с TestServer для более надёжных интеграционных тестов без зависимостей от IIS Express.

Самое быстрое решение – вручную развернуть Shim DLL и зарегистрировать COM‑компоненты, но для долгосрочной стабильности рассмотрите миграцию на .NET Core с использованием TestServer‑базированных интеграционных тестов, которые устраняют зависимости от IIS Express, сохраняя при этом полноценное покрытие кода.

Авторы
Проверено модерацией
Модерация