如果在Docker下运行的.NET Core单元测试的代码覆盖率低于90%,则打破TeamCity中的构建

问题描述 投票:2回答:1

我最近一直在关注Docker,以及如何使用TeamCity在Docker容器中运行.NET Core单元测试作为构建管道的一部分。我将其添加为我的Dockerfile中的最后一行,以便能够运行测试:

ENTRYPOINT ["dotnet", "test", "--verbosity=normal"]

然后在组合文件中引用这些Dockerfiles,TeamCity在命令行中使用docker-compose构建和运行这些文件。

我现在成功地工作了。接下来的挑战是如果单元/集成测试覆盖率小于90% - 或者其他一些值 - 打破构建 - 请不要争论这个!

我成功地使用了coverlet.msbuild NuGet依赖来测量代码覆盖率,作为构建的一部分。这在TeamCity中也可以正常工作,我在TeamCity构建中看到了输出。

我通过将coverlet.msbuild添加到我的每个测试项目中,并将Dockerfile入口点更改为:

ENTRYPOINT ["dotnet", "test", "--verbosity=normal", "/p:CollectCoverage=true", "/p:Threshold=90", "/p:ThresholdType=line"]

TeamCity构建输出显示带有结果的ASCII表,但到目前为止,如果代码覆盖率不够高,我还无法找到打破构建的好方法。如果代码覆盖率太低,TeamCity不会将构建标记为失败,如果它不够通灵,那么它就足够公平了!

我天真地认为我可以在TeamCity中创建一个失败条件,它会检测是否存在以下文本:

'[Assemnbly]' has a line coverage '9.8%' below specified threshold '95%'

...使用这样的正则表达式:

has a line coverage '((\d+(\.\d*)?)|(\.\d+))%' below specified threshold '((\d+(\.\d*)?)|(\.\d+))%'

但是,当测试的DLL引用单独测试的其他DLL时,它会变得棘手,因为coverlet.msbuild报告所有“触摸”DLL的覆盖度量。例如,我有一个名为Steve.Core.Files.Tests的测试项目,测试Steve.Core.Files。但是,Steve.Core.Files又引用了Steve.Core.Extensions。我在自己的测试DLL中单独测试了Steve.Core.Extensions,因此在测试文件时我不关心该DLL的结果。 TeamCity中的输出如下所示:

+-----------------------+--------+--------+--------+
| Module                | Line   | Branch | Method |
+-----------------------+--------+--------+--------+
| Steve.Core.Extensions | 23.5%  | 40%    | 40%    |
+-----------------------+--------+--------+--------+
| Steve.Core.Files      | 100%   | 100%   | 100%   |
+-----------------------+--------+--------+--------+

...所以它基于23.5%的位失败,即使有问题的DLL是100%。这实际上使得使用正则表达式失败条件检查非常困难。

为了使事情进一步复杂化,我使用单个动态Dockerfile在所有程序集中运行所有测试,原因有两个:

  1. 每次添加更多项目和测试时,我都不想更改Dockerfile和docker-compose文件(以及TeamCity)。
  2. DLL之间存在许多依赖关系,因此构建它们并将它们一起测试是有意义的。

这意味着我不愿意将测试分开,以便每个测试都有自己的Dockerfile - 我知道这将允许我使用Exclude / Include标志来获得所需的行为。

有没有人有任何其他想法,我可以解决这个问题吗?

我希望我可以在每个测试项目的级别添加一个文件来告诉它要做哪些DLL覆盖 - 这将是最好的解决方案。如果不这样做,因为我在项目和测试项目之间使用了严格的命名约定,我可以在dotnet test命令中添加一个开关来仅测试与测试程序集同名的程序集减去最后的.Tests位吗?

提前致谢;帮助赞赏!

干杯,

史蒂夫。

2018年9月7日更新:

所以,我的Dockerfiles现在特定于每个单元测试项目。它们看起来像这样并存在于测试项目文件旁边:

FROM microsoft/dotnet:2-sdk

# Set the working directory:
WORKDIR /src

# Copy the solution file and the NuGet.config across to the src directory:
COPY *.sln NuGet.config ./

# Copy the main source project files to the root level:
COPY */*.csproj ./

# Make directories for each project file and move the project file to the correct place:
RUN for file in $(ls *.csproj); do mkdir -p ${file%.*}/ && mv $file ${file%.*}/; done

# Restore dependencies:
RUN dotnet restore

# Copy all files so that we have all everything ready to compile:
COPY . .

# Set the flag to tell TeamCity that these are unit tests:
ENV TEAMCITY_PROJECT_NAME = ${TEAMCITY_PROJECT_NAME}

# Run the tests:
ENTRYPOINT ["dotnet", "test", "Steve.Core.Configuration.Tests/Steve.Core.Configuration.Tests.csproj", "--verbosity=normal", "/p:CollectCoverage=true", "/p:Threshold=95", "/p:ThresholdType=line", "/p:Exclude=\"[Steve.Core.Testing]*\""]

请注意,排除开关应该停止Steve.Core.Testing DLL的覆盖结果,该结果包含在Steve.Core.Configuration的结果中,这是测试的主要依赖项,并且项目正在进行单元测试。

我的compose文件看起来像这样,并且存在于解决方案文件旁边:

version: '3.6'

services:
  # Dependencies:
  steve.core.ldap.tests.ldap:
    image: osixia/openldap
    container_name: steve.core.ldap.tests.ldap
    environment:
      LDAP_ORGANISATION: Steve
      LDAP_DOMAIN: steve.com
      LDAP_ADMIN_PASSWORD: Password1
  steve.core.data.mysql.tests.database:
    image: mysql
    container_name: steve.core.data.mysql.tests.database
    command: mysqld --default-authentication-plugin=mysql_native_password
    environment:
      - MYSQL_ROOT_PASSWORD=Password1
      - MYSQL_DATABASE=testdb
  steve.core.data.sqlserver.tests.database:
    image: microsoft/mssql-server-linux
    container_name: steve.core.data.sqlserver.tests.database
    environment:
      - MSSQL_SA_PASSWORD=Password1
      - ACCEPT_EULA=Y
      - MSSQL_PID=Developer  
  steve.core.email.tests.smtp:
    image: mailhog/mailhog 
    container_name: steve.core.email.tests.smtp  

  # Steve.Core.Configuration:
  steve.core.configuration.tests:
    image: steve.core.configuration.tests:tests
    build:
      context: .
      dockerfile: Steve.Core.Configuration.Tests/Dockerfile
    environment:
      - TEAMCITY_PROJECT_NAME

  # Steve.Core.Data.MySql:
  steve.core.data.mysql.tests:
    image: steve.core.data.mysql.tests:tests
    build:
      context: .
      dockerfile: Steve.Core.Data.MySql.Tests/Dockerfile
    environment:
      - TEAMCITY_PROJECT_NAME

  # Steve.Core.Data.SqlServer:
  steve.core.data.sqlserver.tests:
    image: steve.core.data.sqlserver.tests:tests
    build:
      context: .
      dockerfile: Steve.Core.Data.SqlServer.Tests/Dockerfile
    environment:
      - TEAMCITY_PROJECT_NAME

  # Steve.Core.Data:
  steve.core.data.tests:
    image: steve.core.data.tests:tests
    build:
      context: .
      dockerfile: Steve.Core.Data.Tests/Dockerfile
    environment:
      - TEAMCITY_PROJECT_NAME

  # Steve.Core.Email:
  steve.core.email.tests:
    image: steve.core.email.tests:tests
    build:
      context: .
      dockerfile: Steve.Core.Email.Tests/Dockerfile
    environment:
      - TEAMCITY_PROJECT_NAME

  # Steve.Core.Encryption:
  steve.core.encryption.tests:
   image: steve.core.encryption.tests:tests
   build:
     context: .
     dockerfile: Steve.Core.Encryption.Tests/Dockerfile
   environment:
     - TEAMCITY_PROJECT_NAME

  # Steve.Core.Execution:
  steve.core.execution.tests:
    image: steve.core.execution.tests:tests
    build:
      context: .
      dockerfile: Steve.Core.Execution.Tests/Dockerfile
    environment:
      - TEAMCITY_PROJECT_NAME

  # Steve.Core.Extensions:
  steve.core.extensions.tests:
    image: steve.core.extensions.tests:tests
    build:
      context: .
      dockerfile: Steve.Core.Extensions.Tests/Dockerfile
    environment:
      - TEAMCITY_PROJECT_NAME

  # Steve.Core.Files:
  steve.core.files.tests:
    image: steve.core.files.tests:tests
    build:
      context: .
      dockerfile: Steve.Core.Files.Tests/Dockerfile
    environment:
      - TEAMCITY_PROJECT_NAME

  # Steve.Core.Ldap:
  steve.core.ldap.tests:
    image: steve.core.ldap.tests:tests
    build:
      context: .
      dockerfile: Steve.Core.Ldap.Tests/Dockerfile
    environment:
      - TEAMCITY_PROJECT_NAME

  # Steve.Core.Maths:
  steve.core.maths.tests:
    image: steve.core.maths.tests:tests
    build:
      context: .
      dockerfile: Steve.Core.Maths.Tests/Dockerfile
    environment:
      - TEAMCITY_PROJECT_NAME

  # Steve.Core.Time:
  steve.core.time.tests:
    image: steve.core.time.tests:tests
    build:
      context: .
      dockerfile: Steve.Core.Time.Tests/Dockerfile
    environment:
      - TEAMCITY_PROJECT_NAME

当它在TeamCity中运行时,它只报告来自两个项目的7个测试(出于某些奇怪的原因),即使在12个项目中有236个测试。

如果有帮助,我很乐意通过电子邮件发送TeamCity构建的输出。

有谁知道我怎么能让我的测试全部再次运行呢?

谢谢,

史蒂夫。

docker .net-core teamcity code-coverage xunit
1个回答
0
投票

因此,唯一的解决方案是将每个单元测试项目拆分为自己的compose文件,该文件包含仅测试DLL所需的依赖项。 (例如,用于测试电子邮件DLL的mailhog,用于测试数据库DLL的SQL Server等)。然后,TeamCity使用单个脚本单独运行它们,如下所示:

docker-compose -f docker-compose-configuration-tests.yml up --force-recreate --abort-on-container-exit --build
docker-compose -f docker-compose-configuration-tests.yml down --volumes --remove-orphans

docker-compose -f docker-compose-data-mysql-tests.yml up --force-recreate --abort-on-container-exit --build
docker-compose -f docker-compose-data-mysql-tests.yml down --volumes --remove-orphans

...

每个都有自己的Dockerfile,它构建测试DLL并为单元测试覆盖设置DLL异常。 TeamCity在单个构建步骤中吐出所有测试的结果,然后我在上面的问题中提到的正则表达式代码覆盖失败条件正确地检测到没有达到x%覆盖率并打破构建的测试项目。

现在来研究如何将代码检查(例如FxCop和StyleCop的现代等价物)集成到我的构建过程中......

© www.soinside.com 2019 - 2024. All rights reserved.