近年は GitHub を始めとした Version Control Systems (VCS) のホスティングサービスと連携する形で、プログラミングやソフトウェア開発に関する様々な Web サービスが提供されています。私は専ら Windows のデスクトップ・アプリケーションを開発していますが、この領域においても便利な Web サービスが数多く登場してきました。そこで、この記事では、私が Windows アプリを開発する際に利用している Web サービス (AppVeyor, Codecov, Codacy) について纏めていこうと思います。
AppVeyor
AppVeyor は Windows サーバ上で提供される継続的インテグレーション (CI: Continuous Integration) の Web サービスです。CI サービスとしては Travis CI や Circle CI などが有名ですが、AppVeyor は Windows や .NET Framework、Visual Studio などのサポートが充実している事もあり、Windows アプリやサービスの開発者を中心に利用者を増やしているようです。
私が実際に CI と言うものに触れたのは AppVeyor が初めてでしたが、利用して感じた利点は主に以下の 2 つです。
- ビルド結果のスナップショットが自動的に保存されていく
- ローカル開発環境とは異なる環境でビルドおよびテストを実行する事ができる
VCS と言う概念が普及した事もあり、ソースコードに関しては私も長らく Git を用いてバージョン管理を行っています。しかし、最終的なビルド結果に関しては、残念ながら管理がおざなりになる傾向にありました。そのため、何らかの理由で以前のバージョン結果が必要になった時、特定のタグやコミットを基にして再度ビルドしなければならない事も多く、複数のリポジトリが関係する場合には必要な状態に戻しきれているのか不安な事もありました。AppVeyor は、ビルド結果をダウンロード可能な形で残す事ができるため、これらの手戻り作業が発生せず、必要な時点のスナップショットを確実に取得できるのは大きな利点だと感じます。
また、私の開発環境は、2 台のデスクトップ PC と 1 台のノート PC と言う 3 台構成となっていますが、これらの環境は概ね同じ構成となっているのでローカル環境では気付かなかった不都合が発生する事があります。この問題を幾らか改善する方法の一つとして、AppVeyor 上での実行が役に立っていると実感するようになりました。例えば、(副次的ではありますが)言語設定やタイムゾーンがローカル環境とは異なる設定となっている都合上、その辺りに起因するケアレスミスを手軽に事前確認する手段としても機能しています。
依存関係の解決
ここからは、AppVeyor の設定ファイルを記述する際に躓いた事項について、いくつか記載していきます。尚、これらのほとんどは CubeICE の AppVeyor.yml で問題となった事なので、リンク先の設定ファイルが具体例として良いかもしれません。
まず最初の課題は、ビルド時の依存関係の解決でした。AppVeyor 上でビルドする時、ビルド対象となるプロジェクトが NuGet で公開されているパッケージのみに依存しているのであれば楽なのですが、そうではない場合には少し工夫が必要となります。私が直面した問題は、以下の 2 点でした。
- プライベートまたは公開前の NuGet パッケージに依存する場合
- VC++ で記述されているような非マネージ・ライブラリに依存する場合
CubeICE や CubeRSS Reader が依存している自作ライブラリは、最近まで NuGet 上には公開せずプライベートな状態が続いていました。*1 また、現在においても、これらのプロジェクトの master ブランチは公開前のバージョンを参照している事があるため、何も対策せずにビルドするとエラーが発生します。
この問題に対しては、依存する自作ライブラリも含めて AppVeyor 上で管理する事で解決を図りました。AppVeyor は毎回 NuGet パッケージを生成し、そのパッケージには静的な URL でアクセス可能です。そこで、下記のように NuGet のソースとしてそれらを追加する事で、プライベートな NuGet パッケージを参照する事ができるようになります。
- nuget sources add -name Cube.Core -source https://ci.appveyor.com/nuget/cube.core - nuget sources add -name Cube.FileSystem -source https://ci.appveyor.com/nuget/cube.filesystem - nuget sources add -name Cube.Images -source https://ci.appveyor.com/nuget/cube.images - nuget sources add -name Cube.Forms -source https://ci.appveyor.com/nuget/cube.forms - nuget restore Cube.FileSystem.SevenZip.Ice.sln
次に非マネージ・ライブラリに依存している場合です。これは、自力でダウンロードして出力ディレクトリにコピーする方法で解決を図りました。例えば、CubeICE は 7z.dll と言う 7-Zip のライブラリ (正確には カスタマイズ版) に依存していますが、このライブラリをダウンロードして展開する設定は下記のようになります。
- ps: Start-FileDownload https://ci.appveyor.com/api/projects/clown/7z/artifacts/7z-x64.zip?job=Platform:+x64 - 7z x -o"Applications\Ice\Progress\bin\Release" 7z-x64.zip
尚、カスタマイズ版の 7-Zip ライブラリも AppVeyor で管理している都合上、上記ではその生成結果をダウンロードしています。もし AppVeyor 上で完結できない場合であっても、同様の方法で必要なライブラリをダウンロードして展開する事で、多くのケースは解決できるのではないかと思います。
最終成果物 (Artifacts) の整形
AppVeyor はビルド結果などの最終成果物を Artifacts と呼んでおり、これらは特定の URL からダウンロード可能となります。この Artifacts に関する設定のうち、NuGet パッケージについては "publish_nuget: true" と言う記述だけで自動的に生成してくれますが、それ以外のものについては "artifacts" の項目に記述する必要があります。この項目は、単に生成する事だけを考えるのであれば "path" にビルドの出力ディレクトリを記述すれば OK です。しかし、個人的にはダウンロード用の URL が少し不格好になるのが気になり、何とかならないかとドキュメント等で解決方法を模索していました。
この問題に対しては、最終的には "after_build" の項目で好みの形になるように整形する方法を採用しました。例えば、CubeRSS Reader のメインプログラムを CubeRssReader.zip と言う形で Artifacts に保存する場合、以下のように記述します。
after_build: - xcopy /Y /I Applications\Rss\Reader\bin\Release CubeRssReader artifacts: - path: CubeRssReader
尚、場合によっては "x86" や "x64" のように、設定によって動的に変更される値が必要になりますが、AppVeyor において利用可能なこれらの値は Environment variables に記載されています。例えば、"x64" と言う値が必要な場合は %PLATFORM% と記述します。
複数ブランチを個別に CI 管理
全てのブランチを単一の AppVeyor プロジェクトで管理するのであれば良いのですが、場合によってはブランチ毎に個別に管理したい時もあります。例えば、CubeRSS Reader では WebView に Chrome ブラウザを利用したもの (chrome ブランチ) を実験的に公開していますが、これを AppVeyor 上でも master ブランチとは区別して管理したいと思うようになりました。
この課題には、AppVeyor 上で GitHub(または類似の VCS ホスティングサービス)の同じリポジトリを参照するプロジェクトを複数個作成し、それぞれの設定ファイルの "branches" で対象とするブランチを絞る事で対応するようです。しかし、この場合でも意図した動作になるように設定するのは意外と難しく、最終的には以下のような形になりました。
- master ブランチの設定ファイル (AppVeyor.yml) とは別に、chrome ブランチ専用の設定ファイルを作成する (AppVeyor.Chrome.yml)。そして、AppVeyor.yml の "branches" には master、AppVeyor.Chrome.yml には chrome ブランチのみを対象とするように記述する。
- AppVeyor の各プロジェクトの Settings で、"Custom configuration .yml file name" の項目に対応するブランチの設定ファイル名をそれぞれ入力する。
- 同 chrome ブランチ用プロジェクトの Settings で、"Branches to build" の項目を "On branches specified below" に選択し、その下に chrome と入力する。
問題となったのは 3. に記載した部分です。AppVeyor は GitHub に push したタイミングで、指定された設定ファイルに従って実行するかどうかを決定します。この時、master ブランチには AppVeyor.Chrome.yml が存在しないため、chrome ブランチ用のプロジェクトでは初期設定が利用されます。初期設定では全てのブランチで CI を実行する事になっているので、意図しないブランチの CI 実行結果も反映される事となりました。この誤動作を防止するために、プロジェクトの Settings でも chrome ブランチ以外の実行を防止するように設定しています。
Codecov
Codecov は、GitHub などにホスティングされているリポジトリのテストカバレッジを可視化するための Web サービスです。Codecov 自体は、何らかのユニットテストが実行された結果を表すファイルを解析して表示するものであるため、多くの場合 CI サービスと連携して利用されます。Windows アプリ開発の場合は前述した AppVeyor 上でユニットテストを実行し、その結果をファイルに出力して Codecov に送信します。
Codecov を利用するようになって最も良いと感じているのは、プログラムを 行単位で テストが実行されたかどうかを確認する事ができると言う点です。例えば、引数を伴うメソッドを実装する場合、最初に引数の null チェックを記述しがちですが、Codecov 上で確認すると、該当の行が黄色(C1 カバレッジが未達成)のままな事があります。
この時、まず最初に検討するのはテスト不足ですが、同時に「この null チェックは不要なのではないか?」と言う可能性についても検討します。そして、呼び出し元なども確認した結果、該当のチェックが不要であると確認できた場合には、単純に消去するか、もしくは assert 文に置換します。このように、自分のプログラムを行単位でテストが実行されているかどうかを定期的に確認すると言う行為が、セルフ・コードレビューのような機能も果たす事が分かりました。
カバレッジの数値との付き合い方
カバレッジの数値、例えば 80% や 100% のような値をどう捉えるかに関しては、当初よりもかなりポジティブな印象を抱いています。例えば、「enum に対応する文字列を取得する」ような単純なメソッドのテストコードに対しては、長らく「このテストに、カバレッジの数値を上げる以外の意味があるのだろうか?」と言う疑問を抱いていました。しかし、これらのテストコードによって、リファクタリング時の Typo による思わぬバグを早期に発見できた言う経験が何度もあり、一見すると無駄に見えるテストの重要性を実感するようになりました。
カバレッジの数値を上げるためだけの近視眼的なテストコードを記述する事によって、それよりも重要なテストコードを見逃すのではないかと言う不安は依然としてあります。ただ最近は、ある程度は仕方がないと割り切り、以下のような指針で実装およびテストコードの記述を行っています。
- 初期リリースまでは、取り合えずカバレッジの数値(80%~90% 程度)を指標としてテストコードを記述する。この結果、初期リリース時点では、正常ケースおよび容易に予想可能な異常ケースのテストに留まる。
- リリース後に何らかの不都合が発覚した場合、必ず最初に該当の不都合が再現するテストコードを記述する。そして、テストを実行してレッド・シグナルを確認する。
- 該当部分の実装コードを修正し、テストを実行してグリーン・シグナルを確認する。
このサイクルによって、完全ではないにしても「少しずつだが、良くなってはいる」事を実感できるのは、テストコードを含めた保守を続けていく、あるいは習慣化する上でプラスに働いていると思います。
AppVeyor との連携設定
前述したように、Codecov は何らかの CI サービスと連携して利用する事がほとんどです。ここでは、AppVeyor との連携方法について記載します。AppVeyor では、まず始めに "test_script" の項目でテストを実行します。私の場合、ユニットテスト・フレームワークは NUnit、カバレッジ計測ツールは OpenCover を利用しており、具体的な記述例は下記のようになります。
test_script: - > packages\OpenCover.4.6.519\tools\OpenCover.Console.exe -register:user -target:nunit3-console.exe -targetargs:Cube.FileSystem.SevenZip.App.Ice.Tests.dll -targetdir:Applications\Ice\Tests\bin\Release -returntargetcode -hideskipped:All -output:CoverResult.xml -filter:"+[*]* -[*]*NativeMethods -[*]*Properties.* -[*]*.Program"
尚、OpenCover の実行プログラムは NuGet 経由で取得するため、テスト対象となるプロジェクトの packages.config に OpenCover を含める必要があります。また、複数のテストを実行する場合 -mergeoutput オプションを記述する事で結果を 1 つのファイルに纏める事ができます。
OpenCover の実行で躓いた点は -returntargetcode オプションでした。このオプションがない場合、テストが失敗した場合であっても正常終了したと見なされるようになります。その結果、不完全な結果が Codecov に送信されてカバレッジの推移が見づらくなると言う問題が発生しました。これに対して -returntargetcode オプションを指定した場合、テストに失敗したタイミングで AppVeyor が実行を中止します。
after_test: - "SET PATH=C:\\Python34;C:\\Python34\\Scripts;%PATH%" - pip install codecov - codecov -f CoverResult.xml
テストの終了結果を Codecov に送信する設定は上記のようになります。Codecov は、プロジェクト毎にアップロード用 API key を割り当てるのですが、AppVeyor 経由だと API key は必要ないようです(ローカル環境からアップロードしようとした場合は必要)。
Codacy
Codacy は、コードレビューの自動化 Web サービスです。GitHub に push したタイミングで対象となるリポジトリのプログラムに対して何らかの静的解析が実行され、その結果を Web 上で閲覧する事ができます。Supported Languages によると、対応しているものは Scala、Java、JavaScript、Python、Ruby、PHP を始めとした 14 種類で、この中に C# も含まれています。また、Codacy はカバレッジのレポート機能も有していますが、現状では残念ながら C# のプロジェクトには適用できませんでした。将来的には、Codecov で行っている事も Codacy に統合できるかもしれません。
私が Codacy の存在を知ったのは数ヶ月前の事で、まだそこまで使いこなせている訳ではないのですが、なかなか良い感触を得たため、自分のプロジェクトに対して順次適用しています。Codecov の項目でも少しだけ触れましたが、ほとんど 1 人で開発を行っているとコードレビューを行う人がそもそも存在しないため、定期的なセルフ・コードレビューを意識する必要があります。Codacy は、この課題を補う方法の一つとして機能していると感じます。
Codacy は解析の結果、問題のある項目を Issue として登録します。そして、プログラマがその Issue を解決する方法としては、以下の 4 つが挙げられます。
- プログラムを修正する
- 該当の Issue を無視する (Ignore issue)
- 登録された Issue を特定するのに使用されたルールを無視する (Remove pattern)
- 該当ファイルの全ての Issue を無視する (Ignore file)
基本原則としては、プログラムを修正する事によって Issue の解決を図るべきとは思います。ただ、実際にやっていると様々な理由で、それだけでは行き詰まる事があるのも事実です。ここでは、私がこれまで「無視」を選択した Issue について、いくつか記録を残しておこうと思います。
Unused 系の Issue をどう扱うか
私が Codacy を数ヶ月利用した中で、最も判断に迷うのが Unused 系の Issue の扱いについてでした。Codacy は、未使用の変数やメソッドを発見した場合、それを Issue に追加します。これ自体は良い事で可能な限り修正(削除)すべきですが、問題になったのはその精度でした。
私も Issue が登録された場合、まずは自分の側に問題があると考えて解決方法を模索しますが、Unused 系に関しては、現時点の自分の知識ではどう考えても誤動作ではないかと思われる Issue が大量に登録されます。代表的なものは以下の 3 点です。
- public な class の public なプロパティであり、該当のプロパティを使用しているテストコード等が存在するにも関わらず "Remove the unused private property" と言う Issue が登録されてしまう
- 定義した次の行に変数を使用している記述があるにも関わらず "Remove this unused 'foo' local variable" と言う Issue が登録されてしまう
- イベントを定義すると、同クラス内で Invoke する記述が存在し、該当イベントを利用しているテストコード等が存在するにも関わらず "Events should be invoked" と言う Issue が登録されてしまう
これらに関しては、今のところ「十分に検討したが、やはり誤動作だろう」と思ったものについては個別に Ignore issue を選択しています。C# の解析には Sonar C# が用いられているとの事なので、余裕があればそちらの方も少し見てみようかと思います。
その他に無視した Issue 一覧
上記以外で無視している Issue (Remove pattern) は以下の通りです。これらに関しては、音楽性の違いに依るものもあるので、無効にせず修正した方が良いと感じる人もいるかもしれません。
- Empty "default" clauses should be removed
"default: break;" と言う記述があると登録される Issue です。元々、switch 文の処理のない default 句は書かないようにしていたのですが、default 句は必ず記述する旨の Issue が登録されたので対応した所、今度は上記の Issue が登録されてしまいました。現状では、こちらの Issue を無視しています。 - Control structures should use curly braces
if 文や while 文などのブロック部分には必ず中括弧を記述すべきだと言う Issue です。個人的には、条件文とブロック部分の間に改行を挿入するスタイルは予期せぬバグを埋め込む可能性があり NG ですが、「1 行で書ききれるのであれば、中括弧は省略しても良い」と言う思想なので(if (condition) { return; } とは書きたくない)このIssue は無視しています。 - Members should not be initialized to default values
private object _member = null; のような記述はやめろと言う Issue です。null に関しては Issue に従って削除していますが、bool や int 等に関しては「その値をソースコード上で明示しておく事に意味がある」と感じる事も多いので、ケース・バイ・ケースで個別に無視しています。 - Methods and properties that don't access instance data should be static
あるクラスのメソッドが、そのクラスの状態にアクセスしないのであれば static にすべきだと言う Issue です。プログラムの見やすさを改善する意味で作成する private メソッドで指摘される事が多いようです。指摘内容は分からなくもないのですが、private メソッドはちょっとした修正で状態にアクセスする必要が出てくる事も多いので、この Issue は無視しています。
以上、私が現在の開発で利用している Web サービスの紹介でした。
*1:クラスやメソッドなどの公開インターフェースに対して破壊的な変更をまだまだ行っていたので、公開版とするのは時期尚早と考えていました。