Docker
.NETCore
xUnit
BitbucketPipelines

.NET Core + SQL Server のユニットテストを Bitbucket Pipelines で動かしてカバレッジレポートを作成する

Java から .NET Core に入門した身としては CI をどうするかはよくわからない問題です。

VSTS や Visual Studio にべったり依存すれば方法はあるのかもしれませんが、

なるべく GUI ツールではなく、テキストベースのスクリプトでビルドやテストができるような方針でありたいものです。

そこで最初に考えたのは、 Jenkins 2 系の Jenkinsfile のビルドスクリプトを使ったものでした。

当時は .NET Core Linux 向けのカバレッジ計測が出させるライブラリがなく、Windows サーバーで動かし OpenCover を使っていました。

Ver.UP で動かくなったり、ライブラリの導入自体が面倒だったりでJenkins 自体のメンテが面倒になってきました。

先日ふと調べたところ、 coverlet という .NET Core でクロスプラットフォームのカバレッジ計測ツールが出ていることを知り、CI 環境を 今使っている Bitbucket Pipelines に乗り換えることにしました。

諸事情により Bitbucket 側のリポジトリはプライベートにしているのでサンプルコードは github で公開します。

https://github.com/kencharos/pipeline.sample


テスト対象のクラス

副作用のない処理をするクラスと、 EntityFramework を使う副作用のあるクラスをテスト対象にします。

    public static class SomeUtil

{
public static int SomeAbs(int num)
{
// ブランチ分岐計測のための冗長な書き方
if (num >= 0)
{
return num;
} else
{
return -num;
}
}
}

    public class PersonService

{
readonly SampleDbContext _ctx;

public PersonService(SampleDbContext ctx)
{
_ctx = ctx;
}

public async Task<IEnumerable<Person>> FindAll()
{
// Personテーブルから全部取得
return await _ctx.Person.ToListAsync();
}
}


テストクラス

coverlet を使うため、テストプロジェクトの csproj には coverlet.msbuild を追加します。

  <PropertyGroup>

<TargetFramework>netcoreapp2.1</TargetFramework>
<DebugType>full</DebugType>
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<!-- dotnet test /p:CollectCoverage=true が使用可能に -->
<PackageReference Include="coverlet.msbuild" Version="2.1.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.7.0" />
<PackageReference Include="xunit" Version="2.3.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" />
<DotNetCliToolReference Include="dotnet-xunit" Version="2.3.1" />
</ItemGroup>

最初のstaticメソッドのテストクラスは次のようになります。

    public class SomeUtilTest

{
// Theory でパラメタライズ
[Theory]
[InlineData(42, 42)]
[InlineData(-42, 42)]
[InlineData(0, 0)]
public void TestAbs(int input, int expect)
{
Assert.Equal(expect, SomeUtil.SomeAbs(input));
}
}

次の データベースを使ったテストは次のようになります。

テスト実行前後でデータベースの生成・データ投入と 破棄を行っています。

    public class PersonServiceTest :IDisposable

{
readonly SampleDbContext _ctx;

// ローカルまたは Docker の SQLServer への接続文字列。あとで環境変数などから取得するようにする
readonly static string ConnectionString = "Server=127.0.0.1,1433;Database=PersonServiceTest;User=sa;Password=SqlServerPass01";

public PersonServiceTest()
{
var builder = new DbContextOptionsBuilder<SampleDbContext>()
.UseSqlServer(ConnectionString);

_ctx = new SampleDbContext(builder.Options);

// create database from entity classes if absent.
_ctx.Database.EnsureCreated();
// insert initial data;
_ctx.Person.Add(new Person { Name = "name1", Age = 11 });
_ctx.Person.Add(new Person { Name = "name2", Age = 12 });
_ctx.SaveChanges();
}

public void Dispose()
{
// delete databse.
_ctx.Database.EnsureDeleted();
}
[Fact]
public async Task TestFindAll()
{
var target = new PersonService(_ctx);
Assert.Equal(2, (await target.FindAll()).Count());
}
}


ローカルでの実行

まずは SqlServer を立ててみます。

なお、SqlServer でなく、モックやインメモリデータベースを使う方法もありますので、本来であればテストの目的や内容によって外部依存の実現手段はいろいろ変えていきます。

Docker で立てるのなら次のようにして、マスターパスワードを設定して起動します。

docker run -e "ACCEPT_EULA=Y" -e "SA_PASSWORD=SqlServerPass01" -p 1433:1433 -d microsoft/mssql-server-linux:2017-CU8

注意点として、 SqlServer の Docker イメージは最低でも 2G RAMが必要なことと、起動に10数秒かかることが挙げられます。

https://docs.microsoft.com/ja-jp/sql/linux/quickstart-install-connect-docker?view=sql-server-linux-2017

正直なところ SqlServer の Docker イメージはテスト用途では若干重厚で、ここは今後改善が必要だなと思っているところです。

dotnet コマンドでテストプロジェクトを指定してテストを実行します。

dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=lcov PipelineSample.Test

p:CollectCoverage はcoverlet によるカバレッジ取得、p:CoverletOutputFormatはカバレッジレポートの出力とそのフォーマットです。

テスト終了後標準出力にもカバレッジサマリが出ます。

+----------------+--------+--------+--------+

| Module | Line | Branch | Method |
+----------------+--------+--------+--------+
| PipelineSample | 100% | 100% | 100% |
+----------------+--------+--------+--------+


カバレッジレポートの HTML 化

coverlet は カバレッジレポートを、 json(istunbul)、 opencover、 lcov、Covertura などの形式で出せます。

opencover 形式で ReportGenerator を使えば HTML にできそうですが、 ReportGenerator を Docker で動かせるかよくわからなかったので、今回は lcov 形式にして genhtml 出だすようにしました。

ほかにいい方法知っている人がいたらぜひ教えてください!

genhtml PipelineSample.Test/coverage.info --output-directory out --branch-coverage

こんな感じのレポートが出ます。

2018-07-28_21h34_53.png


(追記)

結局、HTMLレポート作成しても zip に固めて置いておくだけだとあんまり見ないということがわかりました。

テストレポートをビルドごとに Webページで見れたり、統計とったりして可視化できないとレポート見て改善しようなんて気持ちにならないです。

coveralls, codecovなどのサービスとの連携を探っています。それならこちらでHTML化する必要なく、 coverlet が出したカバレッジファイルを送ればいいだけですし。


Bitbucket Pipelines で動かす

Bitbucket Pipelines は Bitbucket を使っていれば付属する CI サービスです。

課金はビルドの実行時間で決まり、入っているプランに応じて毎月の無料枠が設けられています。

Bitbucket 上のリポジトリと連携して push のタイミングで任意のスクリプトを実行できます。

スクリプトの実行にあたって必要な環境は Docker イメージで持ってきます。

ビルド手順はリポジトリのルートに "bitbucket-pipelines.yml" というファイルを置けば OK です。

この辺は Travis, Circle CI, gitlab-ci なんかと似てます。

というか gitlab-ci とめっちゃ似てます。

前述した、テスト実行・カバレッジレポート作成と、レポートの転送を行うための yaml は次の通りです。

image: microsoft/dotnet:2.1-sdk-alpine # デフォルトで使用するイメージ

pipelines:
default:
- step:
name: test # テスト実行
script:
- "dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=lcov PipelineSample.Test/"
services: # 同時に別のDocker イメージを使う場合は services を書く。
- db # 定義内容は definitions で。
artifacts: # 後続のステップで参照したいファイルや残したいものは artifacts にする。
- "**/cov*.info"
- step:
name: make report
image: kaskada/lcov # genhtml を使いたいので別の docker イメージを指定
script:
- "genhtml PipelineSample.Test/coverage.info --output-directory out --branch-coverage"
artifacts:
- "out/**"
- step:
name: publish report to bitbucket download
# htmlレポートをzip化して、 bitbucket のファイルストレージに送る。
# https://confluence.atlassian.com/bitbucket/deploy-build-artifacts-to-bitbucket-downloads-872124574.html
image: eamonwoortman/alpine-curl-zip
script:
- "zip -r coverage_${BITBUCKET_BRANCH}_${BITBUCKET_BUILD_NUMBER}.zip out"
- curl -X POST --user "${BB_AUTH_STRING}" "https://api.bitbucket.org/2.0/repositories/${BITBUCKET_REPO_OWNER}/${BITBUCKET_REPO_SLUG}/downloads" --form files=@"coverage_${BITBUCKET_BRANCH}_${BITBUCKET_BUILD_NUMBER}.zip"
definitions: # データベースなど、ビルド以外に使用したい Docker イメージの定義
services:
db:
image: microsoft/mssql-server-linux:2017-CU8
memory: 2048 # サービスの RAMはデフォルト 1G なので、 2G に拡張する。
environment:
ACCEPT_EULA: 'Y'
SA_PASSWORD: 'SqlServerPass01'

step ごとのコメントを見てもらえれば何となく何をやっているかはわかると思います。

テストは dotnet core sdk のコンテナと services で定義した SQL Server のコンテナを用意して行っています。

step で作成されたファイル類は通常だと step 終了で消えてしまいますが artifacts として宣言すると残すことができます。

artifacts は各ステップの実行ごとに復元され、後続の step で参照可能ですし 7日の間はダウンロードできます(ただし、ダウンロードリンクが分かりづらい)。

そのため7日以上残したい場合や外部サービス連携させたい場合に備え最終ステップで 外部のストレージサービスなどに永続化するようにしました。

今回は Bitbucket が提供するストレージに送りましたが、 S3なんかでもいいでしょうし、 coveralls, codecov のような テストレポートのための SaaS に送ってもよいと思います。

またテストレポートだけでなく、ビルド結果を AWS や heroku なんかに送ってデプロイなんかも当然できます。

注意点としては、ステップ実行で確保されるメモリが 4G であることです。サービスで別の Docker イメージを使うと按分されます。

SqlServer で 2G を使うと dotnet のテスト側のコンテナで使えるメモリは 2G になります。

ここはテストで行うことが多くなると将来ネックになるかもなとは思います。

定義ファイルで、メモリを2倍にするオプションはあります。

この yaml をプッシュし bitbucket でパイプラインを有効にすると、プッシュごとに自動的にビルドが実行されます。

2018-07-28_21h55_54.png

こんな感じで各ステップが可視化され、ビルド中は標準出力の内容がステップごとに出力されていくようになります。

サービスを使っている場合はサービス側のコンテナのログも別タブで出るので便利です。

出力結果を Bitbucket のストレージに送るようにしたので、次のようにダウンロードページに zipファイルが格納されるようになりました。

2018-07-28_22h00_45.png


課題

というわけで Jenkins から モダンな CIサービスへの移行は何となくできそうだという結論になりました。

たまたま Bitbucket になりましたが、他の CI サービスも 各サービスに応じた yaml ファイルを書けば OK なので多分対応できるでしょう。

一方で課題もあります。


プライベート nuget リポジトリ

本家の nuget.org 以外に社内で nuget のリポジトリを立てている場合は VisualStudio に社内リポジトリの設定を行っていると思います。

この設定を CI サービスからも見えるようにしないといけません。

そのために nuget.config を slnファイルがあるフォルダの直下に置いて、 nuget.config 自体も git で管理するようにします。

VisualStudio が見ている nuget.config は %appdata%\NuGet\ や ~/.config/nuget にあるのでそれを参考またはコピーします。

しかし当然ながらそのリポジトリは BitBucket Pipeilines の IP からアクセス可能にしないといけないので、そこはセキュリティ上注意が必要です。

万全を期すなら nuget リポジトリ自体を Docker イメージ化して、Bitbucket Pipelines のサービスにするのがいいのかなと思っています。

DockerHub 以外の Docker レジストリを参照する手段は用意されています。

https://confluence.atlassian.com/bitbucket/use-docker-images-as-build-environments-in-bitbucket-pipelines-792298897.html?_ga=2.81046103.1508783954.1532603882-462364012.1507279199#UseDockerimagesasbuildenvironmentsinBitbucketPipelines-Useexistingenvironments


テストレポート

カバレッジレポートは出せましたが、テストレポートは出せていません。

dotnet test の弱いところですが、 trx 形式のテストレポートしか出せないんですよね。

trx は Visual Studio でしか読める XML なので XML を解析すれば HTML にはできそうですが、、、

bitbucket pipelies は JUnit のテスト結果XML なら自動的に拾ってくれる機能があるっぽいので、

どうにかそこと上手く連携させるものはないかと探っています。

(追記)

https://github.com/gfoidl/trx2junit

というものを見つけました。 trxファイルを junit xml にしてくれるツールです。

dotnet core 2.1 移行ですが、コンテナ中でグローバルにインストールしてしまえば結構簡単に xmlが出せます。

script:

# loggerオプションで trx を出す。
- "dotnet test --logger:trx\\;LogFileName=../test-reports/result/res.trx"
# trx2junit コマンドをインストール
- "dotnet tool install -g trx2junit"
# コマンドを実行し、trxと同じ場所に junt xml を出力する。
- "/root/.dotnet/tools/trx2junit <各テストプロジェクト>/test-reports/result/res.trx"

これで、 /test-reports//*.xml に一致するファイルを、pipeliens はテスト結果ファイルとみなしてくれるので、パイプライン中に テスト件数などの表示を行ってくれます。

さらにこれを artifact にすれば、テスト結果 HTML にしたり 外部サービスに連携できたりします。

(追記2)

trx2junit が出力するJunit xml はフォーマットがおかしいらしく、 pipeline 上で件数やエラー出力がうまく出ませんでした。

結局次のような XLST を使って trx を自前で変換します。

これだと pipelineも正しく認識してくれるし、 codecovや coveralls のような外部サービスにも送れます。

ただし dotnet test と xlst に2段構えになるので、 テストが失敗してもレポートは作成するようにシェルで一工夫するなどの対処を行いました。

<?xml version="1.0" encoding="UTF-8"?>

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:x="http://microsoft.com/schemas/VisualStudio/TeamTest/2010">
<xsl:template match="/">
<xsl:element name="testsuites">
<xsl:attribute name="name">Test Result</xsl:attribute>
<xsl:attribute name="tests">
<xsl:value-of select="/x:TestRun/x:ResultSummary/x:Counters/@total"/>
</xsl:attribute>
<xsl:attribute name="failures">
<xsl:value-of select="/x:TestRun/x:ResultSummary/x:Counters/@failed"/>
</xsl:attribute>
<xsl:attribute name="errors">
<xsl:value-of select="/x:TestRun/x:ResultSummary/x:Counters/@error"/>
</xsl:attribute>
<xsl:element name="testsuite">
<xsl:attribute name="name">Test Result</xsl:attribute>
<xsl:attribute name="tests">
<xsl:value-of select="/x:TestRun/x:ResultSummary/x:Counters/@total"/>
</xsl:attribute>
<xsl:attribute name="failures">
<xsl:value-of select="/x:TestRun/x:ResultSummary/x:Counters/@failed"/>
</xsl:attribute>
<xsl:attribute name="errors">
<xsl:value-of select="/x:TestRun/x:ResultSummary/x:Counters/@error"/>
</xsl:attribute>
<xsl:attribute name="skipped">
<xsl:value-of select="/x:TestRun/x:ResultSummary/x:Counters/@total - /x:TestRun/x:ResultSummary/x:Counters/@executed"/>
</xsl:attribute>
<xsl:for-each select="/x:TestRun/x:Results/x:UnitTestResult">
<xsl:element name="testcase">
<xsl:attribute name="name">
<xsl:value-of select="@testName"/>
</xsl:attribute>
<xsl:attribute name="classname">
<xsl:value-of select="@testName"/>
</xsl:attribute>
<xsl:attribute name="time">
<xsl:value-of select="@duration"/>
</xsl:attribute>
<xsl:if test="@outcome = 'Failed'">
<xsl:element name="failure">
<xsl:attribute name="message">
<xsl:value-of select="x:Output/x:ErrorInfo/x:Message"/>
</xsl:attribute>
<xsl:value-of select="x:Output/x:ErrorInfo/x:StackTrace"/>
</xsl:element>
</xsl:if>
<xsl:if test="@outcome = 'Error'">
<xsl:element name="failure">
<xsl:attribute name="message">
<xsl:value-of select="x:Output/x:ErrorInfo/x:Message"/>
</xsl:attribute>
<xsl:value-of select="x:Output/x:ErrorInfo/x:StackTrace"/>
</xsl:element>
</xsl:if>
<xsl:if test="@outcome = 'NotExecuted'">
<xsl:element name="skipped"/>
</xsl:if>
</xsl:element>
</xsl:for-each>
</xsl:element>
</xsl:element>
</xsl:template>
</xsl:stylesheet>


テスト実行時間の短縮

SqlServer さえあれば データベースもテーブルも全部初期化できるのが EntityFramework の強みですが初回のデータベース作成が結構遅い(10秒くらい) んですよね。

データベースさえあれば、そんなに遅くないのでデータベースだけ作ったイメージを作るといいのかなと思っています。

あとはデータベースを使ったテストを減らす(モックやインメモリDB) というのも考えられます。

それに SqlServer の Docker イメージが巨大というのも後々問題になるかもしれないですが

そうなったらそうなったときに考えます(多分 Azure SQL Database をテスト用に建てる)。

現在はビルドの時間もそこそこかかっているので問題ないのですが、 SqlServer のコンテナの起動が遅いのでコンテナの準備ができるまで待機するような仕組みをテスト側に入れる必要もあります。


ソリューション単位でのテスト実行

script に、 dotnet test <テストプロジェクト> としているのは実は理由があって、 dotnet test だけでソリューション単位で実行すると通常のプロジェクトに対してテスト無しということでテストが落ちるようになっているんですね。

これで ExitCode が -1 になってパイプラインがエラーになってしまいます。

複数のテストプロジェクトがあると、プロジェクト数分 script 書くのも面倒なんですが、他にもソリューション単位での実行だと(.NET Core 2.1以降?)テスト実行がプロジェクト単位で並列になるから実行時間にも影響するんですよね。

問題だと思っている人は結構いるらしく、 github issue にもありますがまだ Open のままです。

ただその中にテストプロジェクトだけの sln ファイル作ってそれでテストやれば動くというコメントがあり、

一応それで動いたのでいったんはこれで様子見です。

https://github.com/Microsoft/vstest/issues/1129#issuecomment-334608064


ブランチ毎で振る舞いを変える

ブランチによってははビルドだけ、あるいはデプロイを変えたいみたいのは要求としてあると思います。

この方法は bitbucket-pipelines にばっちりありますので、あとはその仕組みに乗っかるだけです。

https://ja.confluence.atlassian.com/bitbucket/branch-workflows-856697482.html


まとめ

Bitbucket Pipelines というどちらかといえばマイナーな CI サービスに、あまり前例のない .NET Core を組み合わせるというニッチな話題を提供しました。

.NET Core 普通に Docker や Linux で動くのはちょっと前だと考えづらいですよね。。

課題もありますが、Jenkins で動かしていた時に比べると CI サーバー自体のメンテナンスを考える必要がほぼないのでやってよかったと思います。