Quantcast
Channel: C#タグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 9738

ASP.NET Core / ASP.NET Web API 2 Owin で Web API の自動テスト環境を整える

$
0
0

概要

C# でバックエンド開発を行う場合、近年では、やはりWeb APIによる開発事例が多いかと思います。
本記事では、C# における Web API 開発フレームワークの代表格である ASP.NET Core、及び ASP.NET API 2 Owin を題材とし、Azure DevOpsを使って自動テスト環境を整えるまでの流れを紹介します。

お品書き

  1. ユニットテストを実装する
  2. Azure DevOpsでビルドパイプラインを作成する
  3. Azure DevOpsのテスト結果をSlackへ通知する

環境

  • Windows10
  • Visual Studio 2019
  • ASP.NET Core
  • ASP.NET Web API 2 Owin
  • Azure DevOps

サンプルコード

サンプルコードを Github にアップしています。
一部、今回の記事と無関係な実装も含まれていますが、ご了承ください。

ASP.NET Core
https://github.com/tYoshiyuki/dotnet-core-mediatr-sample

ASP.NET Web API 2 Owin
https://github.com/tYoshiyuki/dotnet-owin-webapi-sample

ユニットテストを実装する

ASP.NET で Web API を作成した場合、APIのエンドポイントとなる 「コントローラ」 と、業務ロジックを担当する 「ロジック」 のレイヤーを分ける事が多いかと思います。
今回は、上記レイヤー分けに従って、ユニットテストを MsTest 及び xUnit を使って実装していきます。

テストのカテゴリ分けについて

C# のテストフレームワークでユニットテストを実装する場合、テストの内容や種別によってカテゴリ分けをすることをお勧めします。

カテゴリ分けをすることにより、Visual Studio のテストエクスプローラーで表示をフィルタリングしたり、テスト結果の分析を行うことが出来るようになります。ユニットテストの総ケース数が増えてきた場合でも、柔軟なテスト運用が可能となります。テストケース総数を勘案した上で、カテゴリ分けを設計すると良いかと思います。

以下、カテゴリ分けの例になります。

  • 権限の名称

    • 一般ユーザ, 管理ユーザ など
  • テスト種別名

    • ロジックのテスト, Web APIサーバを用いたインテグレーションテスト など
  • 処理時間

    • 通常のテスト, 処理時間の長いテスト
  • 業務ドメインの名称

1. ロジックテストを実装する

MsTest

まずは、ASP.NET Web API 2 Owin / MsTest の実装例です。
テストカテゴリは、アノテーションで指定します。MsTest v1 ではメソッド単位にしかカテゴリを付与できませんでしたが、
MsTest v2 からクラス単位にカテゴリを付与出来るようになっています。

TodoServiceTest.cs
[TestClass][TestCategory("Todo"),TestCategory("Logic")]publicclassTodoServiceTest{// ・・・一部省略

前述した通りですが、テストエクスプローラーでカテゴリに従い、表示項目をフィルタリングすることが出来ます。
テストケース数が膨大になった場合は、有効な機能になるため活用しましょう。
image.png

テストパターンが類似しているテストケースについては、パラメタライズドテストを検討すると良いです。
MsTest v2 はパラメタライズドテストに対応しています。実装例を以下に示します。
インプットデータや期待値を纏めてパラメータとすることで、データパターンを網羅したテストを効率よく実装出来ます。

TodoServiceTest.cs
[DataTestMethod][DynamicData(nameof(TestData),DynamicDataSourceType.Method)]publicvoidUpdate_正常系(Todotodo){// Arrange_mock.Setup(_=>_.Get()).Returns(_data);_service=newTodoService(_mock.Object);// Act_service.Update(todo);// Assertvarexpect=_service.Get(todo.Id);Assert.AreEqual(todo.Id,expect.Id);Assert.AreEqual(todo.Description,expect.Description);}publicstaticIEnumerable<object[]>TestData(){yieldreturnnewobject[]{newTodo{Id=1,Description="Test 991",CreatedDate=DateTime.Now}};yieldreturnnewobject[]{newTodo{Id=2,Description="Test 992",CreatedDate=DateTime.Now}};yieldreturnnewobject[]{newTodo{Id=3,Description="Test 993",CreatedDate=DateTime.Now}};}

上記は、DynamicData のパターンを紹介していますが、それ以外にも DataRow を利用したり、
データソースをカスタマイズしたりと様々な例がありますので、興味のある方は公式ドキュメントを確認することをお勧めします。
https://github.com/Microsoft/testfx-docs

xUnit

次は、ASP.NET Core / xUnit の実装例です。
テストカテゴリは、Traitアノテーションで指定します。MsTestと異なり、キー(name)・バリュー(value)のような形で設定します。

InMemoryUserRepositoryTests.cs
[Trait("Category","Logic")]publicclassInMemoryUserRepositoryTests{// ・・・一部省略

テストメソッドは Fact アノテーション で実装します。

InMemoryUserRepositoryTests.cs
[Fact]publicvoidFindAll(){// ArrangePrepareUsers();varexpect=_users;// Actvarresult=_userRepository.FindAll().ToList();// Assertresult.Count.Is(3);foreach(varuserinexpect){vartarget=result.First(_=>_.UserId.Equals(user.UserId));target.UserId.Is(user.UserId);target.UserName.Is(user.UserName);target.FullName.Is(user.FullName);}}

続いて、パラメタライズドテストの実装例です。パラメタライズドテストには Theory アノテーションを使用します。
MsTest v2 とほぼ同じように記載が可能です。

InMemoryUserRepositoryTests.cs
[Theory][MemberData(nameof(TestData))]publicvoidRemoveTest(Useruser){// ArrangePrepareUsers();varexpect=user;// Act_userRepository.Remove(expect);// Assert_userRepository.Find(expect.UserId).IsNull();}publicstaticIEnumerable<object[]>TestData(){yieldreturnnewobject[]{newUser(newUserId("1"),newUserName("Taro"),newFullName("Taro","Yamada"))};yieldreturnnewobject[]{newUser(newUserId("2"),newUserName("Jiro"),newFullName("Jiro","Suzuki"))};yieldreturnnewobject[]{newUser(newUserId("3"),newUserName("Saburo"),newFullName("Saburo","Tanaka"))};}

インテグレーションテストを実装する

続いて、コントローラから一気通貫でテストを行うインテグレーションテストを実装します。
Web API を呼び出すためには HTTPサーバ が必要になりますが、.NETのユニットテストでは専用の TestServer があるため、これを利用すると、いい感じにテストの実装が出来ます。
image.png

以下、ASP.NET Web API 2 Owin の実装例です。

TodoControllerTest.cs
[ClassInitialize]publicstaticvoidSetup(TestContextcontext){Server=TestServer.Create<Startup>();HttpClient=Server.HttpClient;}[TestMethod]publicasyncTaskGet_正常系(){// Arrange・Actvarresponse=awaitHttpClient.GetAsync(_url);// AssertAssert.AreEqual(HttpStatusCode.OK,response.StatusCode);varresult=awaitresponse.Content.ReadAsAsync<List<Todo>>();Assert.IsTrue(result.Any());}

ASP.NET Core の場合は、WebApplicationFactoryを利用します。
WebApplicationFactory を継承したクラスを準備します。(尚、サンプルソースでは初期データの投入も行っています。)

IntegrationTestWebApplicationFactory.cs
publicclassIntegrationTestWebApplicationFactory<TStartup>:WebApplicationFactory<TStartup>whereTStartup:class{protectedoverridevoidConfigureWebHost(IWebHostBuilderbuilder){base.ConfigureWebHost(builder);builder.ConfigureServices(services=>{varsp=services.BuildServiceProvider();usingvarscope=sp.CreateScope();varrepository=scope.ServiceProvider.GetRequiredService<IUserRepository>();repository.Save(newUser(newUserId("1"),newUserName("Taro"),newFullName("Tanaka","Tanaka")));repository.Save(newUser(newUserId("2"),newUserName("Jiro"),newFullName("Suzuki","Suzuki")));repository.Save(newUser(newUserId("3"),newUserName("Saburo"),newFullName("Sato","Sato")));});}}

次に、WebApplicationFactory をユニットテストで利用した場合の実装例です。
WebApplicationFactory を継承したクラスを、コンストラクタインジェクションで受け取り、そこから HTTP Client を取得し、HTTPリクエストを実行するような感じです。

UsersControllerTest.cs
publicUsersControllerTest(IntegrationTestWebApplicationFactory<Startup>webApplicationFactory){_client=webApplicationFactory.CreateClient();}[Fact]publicasyncTaskGet(){// Arrangeconststringurl="/api/users/1";// Actvarresponse=await_client.GetAsync(url);// AssertAssert.Equal(HttpStatusCode.OK,response.StatusCode);varjson=awaitresponse.Content.ReadAsStringAsync();varresult=JsonSerializer.Deserialize<UserViewModel>(json,newJsonSerializerOptions{PropertyNameCaseInsensitive=true});Assert.Equal("1",result.Id);Assert.Equal("Tanaka",result.FirstName);Assert.Equal("Taro",result.UserName);}

Azure DevOpsでビルドパイプラインを作成する

続いて、Azure DevOpsでビルドパイプラインを作成します。パイプラインの作成方法は、旧形式では GUI で作成する必要がありましたが、現在では YAML での作成が可能となっています。今回は、ビルドパイプラインでユニットテストの実行とカバレッジレポートを取得する方法を紹介します。

まずは、GUIでの作成例です。ASP.NET Web API 2 Owin のプロジェクトをサンプルに作成しています。

image.png

ポイントとしては、テストの実行・カバレッジの取得 (Test and output coverage) と レポートの出力 (Generate coverage report)です。

カバレッジの取得には OpenCover を使用します。Nugetより取得しましょう。

image.png

OpenCover より MsTest をコマンドラインで実行し、カバレッジの取得を行います。
テスト対象となる DLL の指定や、カバレッジ取得対象とする 名前空間 をコマンドラインのパラメータで指定します。
また、カバレッジの取得は デバッグビルド で実行する必要があるため注意が必要です。

image.png

以下、設定内容の詳細 YAML になります。

steps:-script:|%OPEN_COVER_PATH% -register -target:%MSTEST_PATH% -targetargs:%TARGET_FILE_AND_ARGS% -targetdir:%TARGET_DIR% -filter:%FILTER% -output:%OUTPUT_FILE%displayName:'Testandoutputcoverage'env:MSTEST_PATH:"C:\ProgramFiles(x86)\MicrosoftVisualStudio\2017\Enterprise\Common7\IDE\CommonExtensions\Microsoft\TestWindow\vstest.console.exe"TARGET_DIR:".\DotNetOwinWebApiSample.Api.Test\bin\Debug"TARGET_FILE_AND_ARGS:"DotNetOwinWebApiSample.Api.Test.dll/Logger:trx;LogFileName=DotNetOwinWebApiSample.Api.Test.trx"FILTER:"+[DotNetOwinWebApiSample*]*-[*.Test.*]*"OUTPUT_FILE:"coverage.xml"OPEN_COVER_PATH:".\packages\OpenCover.4.7.922\tools\OpenCover.Console.exe"

OpenCover の設定 (特にfilter) に関しては、若干クセがあるので、公式のドキュメントを読んでおくと良いです。
https://github.com/opencover/opencover/wiki/Usage

続いてカバレッジレポートの取得です。
ReportGenerator を Visual Studio Marketplace から取得し、Azure DevOps に追加しましょう。

image.png

上記拡張を追加すると、ReportGenerator のタスクを作成出来るようになります。

image.png

steps:-task:Palmmedia.reportgenerator.reportgenerator-build-release-task.reportgenerator@4displayName:'Generatecoveragereport'

パイプラインの実行後、Code Coverage のタブが追加され、カバレッジレポートが HTML で閲覧出来るようになります。
Tests のタブと合わせて確認することで、効率的にテスト結果を可視化することが出来ます。

image.png

次に、YAMLでの作成例です。
ASP.NET Core のプロジェクトをサンプルに実装します。

trigger:-masterpool:vmImage:'windows-latest'variables:buildConfiguration:'Debug'steps:-script:dotnet restoredisplayName:'dotnetrestore'-script:dotnet build --configuration $(buildConfiguration)displayName:'dotnetbuild$(buildConfiguration)'-task:DotNetCoreCLI@2inputs:command:testprojects:'*.Test/*.Test.csproj'arguments:-c $(BuildConfiguration) --collect:"XPlat Code Coverage" -- RunConfiguration.DisableAppDomain=truedisplayName:Run Tests-task:DotNetCoreCLI@2inputs:command:customcustom:toolarguments:install --tool-path . dotnet-reportgenerator-globaltooldisplayName:Install ReportGenerator tool-script:reportgenerator -reports:$(Agent.TempDirectory)/**/coverage.cobertura.xml -targetdir:$(Build.SourcesDirectory)/coverlet/reports -reporttypes:"Cobertura"displayName:Create reports-task:PublishCodeCoverageResults@1displayName:'Publishcodecoverage'inputs:codeCoverageTool:CoberturasummaryFileLocation:$(Build.SourcesDirectory)/coverlet/reports/Cobertura.xml-task:DotNetCoreCLI@2displayName:'dotnetpublish$(buildConfiguration)'inputs:command:publishpublishWebProjects:Truearguments:'--configuration$(BuildConfiguration)--output$(Build.ArtifactStagingDirectory)'zipAfterPublish:True-task:PublishBuildArtifacts@1displayName:'publishartifacts'

ASP.NET Core の場合、dotnet test のコマンドでカバレッジの取得が可能です。
また、reportgeneratorもコマンドでインストール出来るため、ASP.NET Web API 2 Owinに比べてシンプルにパイプラインが作成出来ます。

.NET Core のパイプラインに関しては、公式にもドキュメントがあります。導入の際には、合わせて確認いただくと良いかと思います。
https://docs.microsoft.com/en-us/azure/devops/pipelines/ecosystems/dotnet-core?view=azure-devops

Azure DevOpsのテスト結果をSlackへ通知する

最後に、Azure DevOps と Slack 連携し、ビルドパイプラインによるテスト結果を通知してみます。
Azure DevOps と Slack は専用の連携アプリがあるため、これを利用します。
詳細な手順は公式のドキュメントがあるため、本記事では割愛します。
https://docs.microsoft.com/en-us/azure/devops/pipelines/integrations/slack?view=azure-devops

設定が完了すると、画像の通りにビルド完了時にSlackへ通知が送付されます。

image.png

しかし、上記通知内容からではテスト成功件数、失敗件数といった詳細な結果を知ることが出来ません。
勿論、メッセージのリンクから Azure DevOps へ遷移する事は可能ですが、今回は通知内容を Incoming Webhooks を利用してカスタマイズしてみます。

上記、ASP.NET Web API 2 Owinのパイプラインを用いて、Incoming Webhooksの送信を作成します。
PowerShellを使って、ビルドパイプライン内でSlackへの通知を行いましょう。

# ---------------------------------------------------#  テスト実行結果のメッセージを構築します# ---------------------------------------------------functionCreateTestResultMessage($file){$filename=$file.name$xml=[XML](Get-Content$file)$start=$xml.TestRun.Times.start$finish=$xml.TestRun.Times.finish$executed=$xml.TestRun.ResultSummary.Counters.executed$passed=$xml.TestRun.ResultSummary.Counters.passed$failed=$xml.TestRun.ResultSummary.Counters.failedreturn"テスト["+$filename+"]の実行結果だよー"+`"`n"+"> 開始時刻: "+([DateTime]$start).ToString("yyyy/MM/dd HH:mm:ss")+`"`n"+"> 終了時刻: "+([DateTime]$finish).ToString("yyyy/MM/dd HH:mm:ss")+`"`n"+"> 実行件数: "+$executed+`"`n"+"> 成功件数: "+$passed+`"`n"+"> 失敗件数: "+$failed}# ---------------------------------------------------#  カバレッジ取得結果のメッセージを構築します# ---------------------------------------------------functionCreateCoverageMessage($file){$filename=$file.name$xml=[XML](Get-Content$file)$sequenceCoverage=$XML.CoverageSession.Summary.sequenceCoverage$branchCoverage=$XML.CoverageSession.Summary.branchCoverage$numSequencePoints=$XML.CoverageSession.Summary.numSequencePoints$visitedSequencePoints=$XML.CoverageSession.Summary.visitedSequencePoints$numBranchPoints=$XML.CoverageSession.Summary.numBranchPoints$visitedBranchPoints=$XML.CoverageSession.Summary.visitedBranchPointsreturn"テストカバレッジ["+$filename+"]の実行結果だよー"+`"`n"+"> Sequence Coverage: "+$sequenceCoverage+"% "+"("+$visitedSequencePoints+"/"+$numSequencePoints+")"+`"`n"+"> Branch Coverage: "+$branchCoverage+"% "+"("+$visitedBranchPoints+"/"+$numBranchPoints+")"}# ---------------------------------------------------#  Slackへ通知します# ---------------------------------------------------functionPostSlack($message){$encode=[System.Text.Encoding]::GetEncoding('ISO-8859-1')$utf8Bytes=[System.Text.Encoding]::UTF8.GetBytes($message)$notificationPayload=@{text=$encode.GetString($utf8Bytes);username="Azure DevOps Test Report";icon_url="https://4.bp.blogspot.com/-CtY5GzX0imo/VCIixcXx6PI/AAAAAAAAmfY/AzH9OmbuHZQ/s800/animal_penguin.png"}$postUri="xxx"# Incoming WebhooksのエンドポイントURLを設定しますInvoke-RestMethod-MethodPOST-Uri$postUri-Body(ConvertTo-Json$notificationPayload)-ContentTypeapplication/json}# テスト実行結果の送信$files=Get-ChildItem-Recurse-File-Include*.trxForeach($filein$files){$message=CreateTestResultMessage($file)PostSlack($message)}# カバレッジ取得結果の送信$files=Get-ChildItem-Recurse-File-Includecoverage.xmlForeach($filein$files){$message=CreateCoverageMessage($file)PostSlack($message)}

ポイントとして、テスト実行結果は .trx ファイル、カバレッジ取得結果は coverage.xml にそれぞれ存在するため、
XMLパーサーを使用して値の読み取りを行っています。カバレッジ取得結果は、利用したツールによってフォーマットが異なっており、OpenCover以外の別のツールを用いた場合は適宜調整が必要なため、ご注意ください。

image.png

また、同様に Microsoft Teams に対してもメッセージの送信が可能です。

# ---------------------------------------------------#  Teamsへ通知します# ---------------------------------------------------functionPostTeams($message){$encode=[System.Text.Encoding]::GetEncoding('ISO-8859-1')$utf8Bytes=[System.Text.Encoding]::UTF8.GetBytes($message)$notificationPayload=@{text=$encode.GetString($utf8Bytes);}$postUri='xxx'# Incoming WebhooksのエンドポイントURLを設定しますInvoke-RestMethod-MethodPOST-Uri$postUri-Body(ConvertTo-Json$notificationPayload)-ContentTypeapplication/json}

image.png

まとめ

後半、C# というよりも Azure DevOps の記事が中心になってしまいました。。。
C#でモダンな継続的開発を行う場合は、ユニットテストの実装とAzure DevOpsの運用がポイントになってくると思います。
本記事が、少しでも皆様の参考情報となれば幸いです。


Viewing all articles
Browse latest Browse all 9738

Trending Articles