SonarQube 也能顯示 Code Coverage

SonarQube 除了能檢查 C# 品質外,還可以當成 dashboard 使用,讓整個團隊有統一入口關注專案的 Code Coverage。

SonarQube 支援 OpenCover 與 dotCover 兩種格式,本文將以 NUnit + Coverlet + OpenCover 介紹。

Version


macOS High Sierra 10.13.6
Docker for Mac 18.06.0-ce-mac70 (26399)
.NET Core 2.1
SonarQube 7.1
SonarScanner 4.3.1.1372
NUnit 3.10.1
Coverlet 2.1.1
Rider 2018.1.3

Coverlet


coverage000

在 NUnit 專案加入 Coverlet package,這是個基於 .NET Core,且能跨平台計算 Code Coverage 的 package。

OpenCover 與 dotCover 算 .NET 生態圈兩大最有名的 package,但目前都只能跑在 Windows 平台,Coverlet 算目前跨平台最佳 solution

Dockerfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
FROM microsoft/dotnet

ENV SCANNER_VERSION=4.3.1.1372
ENV SCANNER_HOME=/opt/scanner

WORKDIR /app

RUN apt-get update
RUN apt-get install -y wget
RUN apt-get install -y unzip
RUN apt-get install -y openjdk-8-jre
RUN wget -q https://github.com/SonarSource/sonar-scanner-msbuild/releases/download/$SCANNER_VERSION/sonar-scanner-msbuild-$SCANNER_VERSION-netcoreapp2.0.zip -O /opt/sonar-scanner-msbuild.zip
RUN mkdir -p $SCANNER_HOME
RUN unzip /opt/sonar-scanner-msbuild.zip -d $SCANNER_HOME
RUN rm /opt/sonar-scanner-msbuild.zip
RUN chmod 775 $SCANNER_HOME/ -R

COPY ./ ./
ENTRYPOINT ["./scanner.sh"]

因為要在 .NET Core container 安裝 SonarScanner,所以要對 .NET Core 客製化 image。

第 1 行

1
FROM microsoft/dotnet

使用 FROM 設定所使用的基底 image。

使用 Microsoft 官方的 microsoft/dotnet image。

第 3 行

1
2
ENV SCANNER_VERSION=4.3.1.1372
ENV SCANNER_HOME=/opt/scanner

使用 ENV 設定 Dockerfile 的環境變數。

SCANNER_VERSION : 因為 SonarScanner 的下載路徑,會與版本有關,所以特別設定成變數。

SCANNER_HOME : 設定 SonarScanner 所安裝的目錄。

第 6 行

1
WORKDIR /app

設定 container 的工作目錄,也就是預設目錄都會在 /app 下。

第 8 行

1
2
3
4
RUN apt-get update 
RUN apt-get install -y wget
RUN apt-get install -y unzip
RUN apt-get install -y openjdk-8-jre

使用 RUN 執行 CLI 指令。

  • 使用 wget 下載 SonarScanner
  • 使用 unzip 對 zip 解壓縮
  • SonarScanner 會使用 Java 執行,需要安裝 Java Runtime

12 行

1
RUN wget -q https://github.com/SonarSource/sonar-scanner-msbuild/releases/download/$SCANNER_VERSION/sonar-scanner-msbuild-$SCANNER_VERSION-netcoreapp2.0.zip -O /opt/sonar-scanner-msbuild.zip

使用 wget 下載 SonarScanner 壓縮檔,並下載到 /opt 目錄下。

13 行

1
2
3
RUN mkdir -p $SCANNER_HOME
RUN unzip /opt/sonar-scanner-msbuild.zip -d $SCANNER_HOME
RUN rm /opt/sonar-scanner-msbuild.zip

建立 /opt/scanner 目錄,將 SonarScanner 壓縮檔解壓縮放到 /opt/scanner 目錄下,解壓縮完刪除 SonarScanner 壓縮檔。

16 行

1
RUN chmod 775 $SCANNER_HOME/ -R

將 SonarScanner 目錄所有檔案賦予執行權力。

18 行

1
COPY ./ ./

將目前 solution 下所有檔案複製到 container 內,也就是 /app 目錄下。

19 行

1
ENTRYPOINT ["./scanner.sh"]

最後會執行專案目錄下的 ./scanner.sh,負責執行 SonarScanner 檢查。

Docker-compose.yml


1
version: "3"

services:
  net-core:
    build: .
    container_name: MyCore
    volumes:
      - "${HOST_DIR}:/code/"
    networks:
      - netcore
    depends_on:
      - sonarqube

  sonarqube:
    image: sonarqube:latest
    container_name: MySonarQube
    ports:
      - 9000:9000
      - 9002:9002
    networks:
      netcore:
        ipv4_address: 172.16.238.10
     
networks:
  netcore:
    ipam: 
      driver: default
      config:
        - subnet: 172.16.238.0/24

設定 .NET Core 與 SonarQube 兩個 container 同時啟動。

第 4 行

1
net-core:
    build: .

使用 build 將同目錄下的 Dockerfile build 成 image,也就是剛才的 Dockerfile

第 6 行

1
container_name: MyCore

使用 container_name 設定 .NET Core 的 container 名稱。

第 7 行

1
volumes:
  - "${HOST_DIR}:/code/"

使用 volumes 設定 host 與 container 的共用目錄,: 左方為 host 目錄,右方為 container 目錄。

目的是能在 .NET Core container 內抓到 host 的 project。

第 9 行

1
networks:
  - netcore

使用 networks 設定 .NET Core 與 SonarQube 共用 netcore 內部網路。

11 行

1
depends_on:
  - sonarqube

.NET Core container 相依於 SonarQube container。

14 行

1
sonarqube:
  image: sonarqube:latest

使用 image 設定 container 所使用的 image。

16 行

1
container_name: MySonarQube

使用 container_name 設定 SonarQube 的 container 名稱。

17 行

1
ports:
  - 9000:9000
  - 9002:9002

使用 ports 設定 host 與 container 所 mapping 的 port,: 左側為 host port,右側為 container port。

為了能在 host 以瀏覽器連上 SonarQube,要將 host 的 9000 與 9002 兩個 port 給 host。

20 行

1
networks:
  netcore:
    ipv4_address: 172.16.238.10

使用 network 設定使用 netcore 網路,並設定其 IP 為 172.16.238.10

一般來說,我們不必為 container 設定固定 IP,只要使用 container 名稱就可彼此溝通,但稍後 SonarScanner 必須使用固定 IP 才能連上 SonarQube,所以在此特別要設定固定 IP

24 行

1
networks:
  netcore:
    ipam: 
      driver: default
      config:
        - subnet: 172.16.238.0/24

設定 netcore 網路。

.env


1
HOST_DIR=~/Code/CSharp

HOST_DIR 以環境變數設定,為 host 與 .NET Core container 所共享的目錄。

scanner.sh


1
2
3
4
5
6
7
8
#! /bin/bash
sleep 35
cd ./ClassLib.Test
dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=opencover
cd ..
dotnet /opt/scanner/SonarScanner.MSBuild.dll begin /k:core2 /n:Core2 /v:1.0 /d:sonar.login=admin /d:sonar.password=admin /d:sonar.host.url=http://172.16.238.10:9000 /d:sonar.cs.opencover.reportsPaths=/app/ClassLib.Test/coverage.opencover.xml /d:sonar.coverage.exclusions=**/Program.cs
dotnet build
dotnet /opt/scanner/SonarScanner.MSBuild.dll end /d:sonar.login=admin /d:sonar.password=admin

使用 SonarScanner 檢查 .NET Core 專案,scanner.sh 會放在專案根目錄。

第 2 行

1
sleep 30

主要是要等 SonarQube 啟動完成,比較好的方式是使用 Health Check 明確得知 SonarQube 已經啟動完成。

這裡暫時先 sleep 30 秒,等 SonarQube 先啟動,.NET Core 才開始執行 SonarScanner。

這裡還有改善的空間,也可以自行調整 sleep 時間。

第 4 行

1
dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=opencover

執行 NUnit 單元測試。

  • /p:CollectCoverage=true : 命令 Coverlet 計算 Code Coverage
  • /p:CoverletOutputFormat=opencover : 命令 Coverlet 以 OpenCover 格式產出

第 6 行

1
dotnet /opt/scanner/SonarScanner.MSBuild.dll begin /k:core2 /n:Core2 /v:1.0 /d:sonar.login=admin /d:sonar.password=admin /d:sonar.host.url=http://172.16.238.10:9000 /d:sonar.cs.opencover.reportsPaths=/app/ClassLib.Test/coverage.opencover.xml /d:sonar.coverage.exclusions=**/Program.cs

使用 dotnet 執行 SonarScanner.MSBuild.dll,一開始以 begin 開頭:

  • /k : SonarQube 對專案的 key,內部將以此 key 作為辨別,必須唯一
  • /n : 在 SonarQube 網頁上顯示的專案名稱
  • /v : 在 SonarQube 網頁上顯示的版本編號
  • /d:sonar.login : 指定 SonarQube 帳號
  • /d:sonar.password : 指定 SonarQube 密碼
  • /d:sonar.host.url : 指定 SonarQube server IP 與 port
  • /d:sonar.cs.opencover.reportsPaths : 設定 OpenCover 格式的 XML 檔案位置
  • /d:sonar.coverage.exclusions : 設定 SonarQube 排除 Code Coverage 計算的檔案

目前 SonarQube 設定 exclusions 的 regex 有些 bug,可能無法如文件那般設定 regex,假如遇到這個 bug,就先用最原始的方式指定 exclusions

第 7 行

1
dotnet build

使用 dotnet build 編譯專案。

Script 語言不用編譯,可以直接使用 SonarQube Scanner 就可以檢查,但 C# 需要編譯,因此必須 dotnet build

第 8 行

1
dotnet /opt/scanner/SonarScanner.MSBuild.dll end /d:sonar.login=admin /d:sonar.password=admin

最後需加上 end,scanner 正式將 dotnet build 檢查出的結果寫入 SonarQube project

  • 仍必須指定 SonarQube 的帳號密碼

執行 SonarScanner


1
SonarScanner $ docker-compose up --build

啟動 .NET Core 與 SonarQube container,並且自動執行 SonarScanner。

  • –build : 重新建立 .NET Core 自訂 image,若有任何修改,將重新包進 image,確保 docker-compose 是最新的 image

core000

coverage002

  1. Coverlet 計算出 Code Coverage,並且產生 coverage.opencover.xml

coverage003

  1. 在 host 以 http://localhost:9000 ,將可看到 SonarQube 的檢查結果,並且正確顯示 Code Coverage

結束 Container


1
$ docker-compose down

core003

Conclusion


  • 由於 SonarScanner 已經被包在 Dockerfile 內,host 就不用再安裝 SonarScanner
  • 沒有將 .NET Core 安裝在 SonarQube container 內,而是分別使用 .NET Core container 與 SonarQube container,符合 Microservice 精神
  • Coverlet 為目前唯一的 .NET Core 跨平台 Code Coverage 解決方案,並且可以產出 OpenCover 格式 XML
  • 只要執行 docker-compose up --build,就會執行 NUnit 單元測試與 Coverlet 計算 Code Coverage,且 SonarScanner 也會自動進行檢查

Sample Code


完整的範例可以在我的 GitHub 上找到

Reference


Tonerdo, Coverlet
Shryne Boyer, Cross platform code coverage arrives for .NET Core
SonarQube, Code Coverage Result Import (C#, VB.NET)