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



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

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

然后,这些Dockerfile在compose文件中被引用,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的覆盖率指标。例如,我有一个名为Steven.Core.Files.Tests的测试项目,它测试Steven.Core.Files。然而,Steven.Core_Files反过来引用Steven.Core.Extensions。我在自己的测试DLL中单独测试Steven.Core.Extension,所以在测试文件时我不在乎该DLL的结果。TeamCity中的输出如下:

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

因此它基于23.5%的比特失败,即使所讨论的DLL是100%。这实际上使得使用Regex故障条件检查非常困难。

更复杂的是,我使用一个动态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]*""]

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

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

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构建的输出有帮助的话,我很乐意通过电子邮件发送。

有人知道我怎样才能让我的测试重新开始吗?

谢谢,

史蒂夫。

因此,唯一的解决方案是将每个单元测试项目拆分为自己的组成文件,其中包括该测试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在一个构建步骤中显示所有测试的结果,然后我在上面的问题中提到的regex代码覆盖率失败条件正确地检测到没有达到x%覆盖率的测试项目并破坏了构建。

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

最新更新