パッケージソフトウェア開発とCI

この記事は [セゾン情報システムズ Advent Calendar 2022](https://qiita.com/advent-calendar/2022/sisco) 4日目の記事です。

弊社製品 [HULFT Cloud Storage Option](https://www.hulft.com/software/hulft-8/hulft-cso) の開発で活用しているCI環境について紹介します。
試行錯誤しながら改善を勧めている途中でまだまだ課題もありますが、手ごたえを感じている部分もあるので、パッケージソフトウェア開発におけるCI活用について共有できればと思い記事にしました。

## そもそもCIとは

CIは継続的インテグレーション（Continuous Integration）の略です。
コードに対する変更に対し自動テストなどを使用して品質を保証することで、常に最新状態のコードを正常に保ちつつ、小さい単位での変更を素早く継続的に行っていく手法です。

弊チームでは特に自動テストによる品質保証の部分に重きを置いています。

## CI導入の経緯、自動テスト文化の形成

製品企画立ち上げ時に参画していたテックリーダーの意向により、プロトタイプ作成を開始した段階で[Jenkins](https://www.jenkins.io/)を使用したCI環境の作成を開始していました。導入完了後は毎日の朝会や振り返りの際にJenkinsの画面を確認し、テストの失敗やカバレッジを確認する時間を設けました。テストの失敗やカバレッジの低下を発見すればすぐに担当を割り振り修正するようにし、常に健全な状態を保つ体制がつくられました。
初めはテストコードという考え方を知らなかった新人の私も、先輩方がペアプロで実際にコードを書きながらテスト駆動開発の考え方を教育してくれたおかげで徐々にテストコードを書くという習慣がついていきました。

CI環境は作って終わりではなく、作った後に維持していく体制を作ることが重要だと思います。

## CI導入の効果

上記の製品企画立ち上げから5年が経ちます。
紆余曲折ありましたが無事製品化にたどり着き、これまで7度のリリースを行うことが出来ました。
当時所属していたチームメンバーで残っているのは私だけですがCI環境は現在も稼働しています。
開発の中で私が感じたCI導入の効果は大きく分けて以下の3つです。

 1. 高いモジュール品質の維持に貢献
 1. テスト工数の削減によるプロジェクト期間の短縮に貢献
 1. 自動化による作業の属人化の防止、履歴管理に貢献

### 高いモジュール品質の維持に貢献

1. 新機能実装時にユニットテストも同時に作成することで、今までPTとして手動で行っていた内容がコードとして残ります。これによりコードレビューと同時にPT内容のチェックが行えるようになりました。
1. 一度実装するとCIにより継続的にテストされるので、それ以降のコード修正が心理的に格段に行いやすくなります。
1. テストケースの観点として各機能の仕様が明記されるので、設計書の仕様理解の相違を検出しやすくなります。

コード量が増えてしまうため、レビューにかかる労力は増えてしまいますが、その分コードに含まれる情報が増えます。コードレビューの際はテストコードと設計書を比較して確認することで、実装漏れなどを防ぐことが出来ます。
副次的な効果として、レビューをしやすいようになるべく実装を分割して細かくコミットする意識が向上しました。

### テスト工数の削減によるプロジェクト期間の短縮に貢献

1. モジュール作成後のテスト工程ではデグレ確認を行うリグレッション項目だけでも1000近い件数があります。これを毎プロジェクト実施していたため、小さな機能改修を行うだけでも一カ月弱のテストが必要な状態でした。CIによる自動結合テストによるリグレッションを実施することで、この工数を大幅に縮小することが可能になりました。
1. テスト工程前に事前にユニットテストによるチェックをしっかり行えていることにより、モジュール実装時のバグはほとんど残っておらず、検出される障害は環境起因のものや仕様バグがほとんどで、障害修正による手戻りや遅延がほとんどない状態となっています。
1. 自動結合テストは設計書があればモジュールの実装が終わっていなくとも事前に作成することが可能であるため、モジュールの実装と並行して進めておくことでモジュール作成後すぐに自動テストを実施しすることが可能で、障害の早期検出、テスト期間の短縮が可能になります。

自動テストはシンプルなオペレーションのテストで特に効果を発揮しています。
本製品の場合は、ユーティリティに一つオプションを増やした場合、入力値のチェックで10件程度のテスト項目を用意します。この時増える実施テスト項目は単純に10件ではありません。本プロジェクトで作成されるテスト対象のモジュールは一つではなく、接続先のストレージが3種類、稼働するOSが2種類の組み合わせで6モジュールになります。つまり 3×2×10 = 60 項目実施する必要があります。
自動テストの仕組みを作成しておくと、10件の項目定義を追加するだけですべてのモジュールに対して自動テストを行うことが出来ます。新機能の場合は人によるテストを全く行わないのはリスクですが、自動テストである程度の品質が保たれた状態であれば各モジュール2項目ずつの実施で十分品質を担保できると考えています。全て合わせても12件です。このように人によるテスト項目実施を削減することが出来ます。

### 自動化による作業の属人化の防止、履歴管理

1. Jenkinsジョブでモジュールの作成を行うことにより、誰でも簡単にモジュールを作成することが可能になりました。
2. Jenkinsジョブでモジュールの作成を行うことにより、モジュール作成の履歴を管理することが可能になりました。

Jenkinsはモジュール作成時のコンソールログ、直近のコミット情報などを保存しておくことが可能です。ビルドジョブの中で各種ツールのバージョン情報を表示するようにしておくことで、環境やビルドスクリプトに手を加えていた場合でも、コンソールログを後から確認することでモジュールがビルドされた際の環境情報まで確認することが出来ます。

## CIを維持するのに効果的だったこと

個人的に良かった施策として以下があります。

1. コードカバレッジ、テストケース数の目標値をテスト計画に含める
1. JenkinsによるSlack通知はエラー時のみにする
1. テストコードを実装する余裕がない場合でも空のテストケースを作成する

### コードカバレッジ、テストケース数の目標値をテスト計画に含める

リリース毎に作成するテスト計画書に自動テストについての数値目標も入れておくことで、自動テストをあればいいものではなく、なくてはいけないものにすることが出来ました。また、工数の関係上どうしても自動テストの実装を後回しにするしかない場合も、計画に含めておくことで振り返りの際に話題に上がり今後の計画を建てることが出来ます。

### JenkinsによるSlack通知はエラー時のみにする

時間がかかるジョブの場合、毎回結果を確認しに行くのは大変なのでSlackと連携するのは非常に効果的です。
ですが、正常完了の情報とエラー情報を同じ通知方法にしているとつい結果通知を読み飛ばしてしまいがちです。エラー時にすぐ気づくことが出来るように特別な通知方法にしておくと効果的です。
弊チームではJenkinsのジョブの実行結果が前回実行時と変わったときのみ通知する設定を使用しています。

### テストコードを実装する余裕がない場合でも空のテストケースを作成する

時間が無くてテストを作成できない。という言葉は必ず聞くことになると思います。ですがその場合でも必ずテストコードは作成するルールとしています。弊チームで使用しているGo言語の場合はエディタ上でワンクリックするだけでテストコードのひな型を作成してくれます。これだけであれば一分で対応できます。実施されるテストケースがない場合でも空のテストコードと対応予定のコメントを残しておくだけで今後の保守での対応が行いやすくなります。これが行われていない場合、レビューする際に対応漏れか判断できませんし、その後の対応でテストコードを追加する場合もどこに追加する必要があるのかコード全体のカバレッジを確認する必要があります。
テストコードを書くのに慣れていれば簡易的な動作確認も手ではなくテストケースで行うので最低限のテストケースは作成される文化になるはずです。

## システム構成

ここからは実際に使用しているCI環境について説明します。
現在は以下の用な構成で開発を行っています。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2997186/5ceff57e-deb1-79e9-ff1b-17cd4ec2a82f.png" width=600 />

### 各コンポーネントの役割
|コンポーネント|説明|
|:-|:-|
|開発環境|コーディング環境。vimやvscodeなどそれぞれの好みの開発環境で開発しています。|
|GitHub|コード管理に使用しています。全社方針に合わせてBitbucketやGHEを使用していたこともありましたが、GitHubに落ち着きました。|
|Jenkins|マージされた製品コードのテスト、製品モジュールの作成に使用しています。|
|Jira|障害情報や新規実装機能などのタスク管理に使用しています。|
|Slack|JenkinsやGitHubの通知に使用しています。|
|ストレージ|最終的な成果物である製品モジュールを保存しています。|

重要な役割を担っているのはJenkinsで、ここでコードの品質を担保しています。
弊チームではレビュー完了後にマージされたコードだけをこの環境でテストしています。テスト実行にそれなりの負荷がかかるためこのような運用になっています。そのぶん、開発者はそれぞれのローカル環境でテスト実行を行ってからレビュー依頼を行うルールとなっています。このため開発環境でも簡単にテスト実行が行える仕組みづくりも含めてCI環境整備を行っています。
Jenkins環境でのテスト実施は開発者の環境に依存したバグや他モジュールへの意図せぬ副作用を検知する役割です。ですが実際に開発を行っていると、コードマージ後にJenkins環境で不具合が発覚することがそれなりの頻度で発生します。そこで、プルリクエストでレビュー時に簡易的なチェックをする機構をGitHubActionsのワークフローとして新しく増やしました(※)。まだ導入したばかりのため効果は確認できていませんが、次のプロジェクトではコードマージ後の不具合発覚が少なくなることを期待しています。
※会社で用意されているセルフホストランナーを使用


## 作成モジュール（成果物）

作成するモジュールはHULFT8から読み込まれる共有ライブラリと設定用のユーティリティとなっています。
これらのモジュールに対してCIにて自動テストを実施しています。
また、リリースモジュールの作成ジョブではこれらのモジュールとドキュメントを合わせてZIPファイルを作成します。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2997186/60394463-d5f5-51a8-0607-bb7a029c53e4.png" width=600 />

## 自動テスト内容

### 要素技術

以下の言語、フレームワーク、ツールを使用しています。

|言語|用途|
|:-|:-|
|Go言語|共有ライブラリ及びユーティリティはGo言語で開発を行っています。|
|C言語|共有ライブラリの作成にcgoを使用しているため一部はC言語で開発を行っています。|
|C++|Cで書かれたコードのユニットテスト実装に使用しています。|
|Java|作成したモジュールの自動結合テスト実装に使用しています。|

|フレームワーク|説明|
|:-|:-|
|[Catch2](https://github.com/catchorg/Catch2)|C++のユニットテストフレームワーク|
|[JUnit4](https://junit.org)|Javaのユニットテストフレームワーク|

|ツール|説明|
|:-|:-|
|[make](https://gnuwin32.sourceforge.net/packages/make.htm)|ビルドジョブ、テスト実行のスクリプトはshellとMakefileで管理しています。|
|[golangci-lint](https://golangci-lint.run)|GoのLinter管理ツール|
|[go-junit-report](https://github.com/jstemmer/go-junit-report)|go test の出力をJenkinsで集計できる形に変換するツール。もともとは [go2xunit](https://github.com/tebeka/go2xunit) を使用していましたがアーカイブになったため乗り換えました。|
|[gocover-cobertura](https://github.com/t-yuki/gocover-cobertura)|go test のカバレッジをJenkinsで集計できる形に変換するツール|
|[valgrind](https://valgrind.org/)|Cコードのメモリリーク検出ツールとして使用|
|[lcov](https://github.com/linux-test-project/lcov)|Cコードのカバレッジ取得ツール|
|[cloc](https://github.com/AlDanial/cloc)|コード行数を集計するツール|

### ユニットテスト

開発用メインブランチにコードがマージされるとモジュール作成前に関数単位のテストを実行します。
Go言語は言語組み込みでテスト実行機能があるのでそれを利用し、テスト結果とカバレッジをJenkinsに合わせて整形しています。C言語の場合はCatch2フレームワークを使用しています。
テストが正常に完了した場合はモジュールをビルドし、後続の結合テストを実行します。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2997186/2eb5c81e-b728-f545-ef21-141e091e35b0.png" width=600 />

プロジェクトではテスト結果のコードカバレッジを評価指標にしていて、弊チームでは70％を維持しなければいけない指標としています。実際の運用ではコード変更時は変更前よりもカバレッジが高くなるように意識して開発しているので現在のコードカバレッジは79%ほどになっています。

### 結合テスト

JavaとJUnitを使用したモジュール呼び出しテストです。共有ライブラリはJNAを使用し呼び出しを行い、ユーティリティはコマンドラッパーで入出力パラメータを操作しテストを行います。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2997186/02fdae8f-80aa-0e67-b6c4-e5bb3c3b9fef.png" width=600 />

テストケースが増えると実行に時間がかかってしまうため、テストをクラス分けし実行ジョブを分けて管理しています。大きく分けてスケジュール実行で定期的に実行されるジョブとモジュールが作成された場合すぐに実行されるジョブの2種類です。
コアな機能の部分はコードに変更があった場合すぐに実行されるテストとして用意しています。コード変更後30分以内に結果が出るのが望ましいと思います。
変更が少ない部分、処理に時間がかかるものはスケジュール実行で1日1回深夜に実行されるように調整しています。もちろん、変更を行った結果をすぐに確認したい場合は手動で実行することも可能です。マシンのスペック上、時間がかかるテストが長時間動いていると他のテスト実行に影響が出てしまうためこのような構成としています。

プロジェクトではテストケース追加数数を評価指標にしています。
結合テストのテストケース管理は特に悩ましい問題です。前述したように、結合テストを自動化することで人によるテスト工数を削減することが出来ますが、そのためには自動テストがどの機能、観点を網羅しているのかをテスト項目を作成するテスト管理者が正確に把握できる必要があります。弊チームでは設計者、実装者、テスト管理者、自動テスト担当者の役割を専属で行うのではなく、チーム内でプロジェクトごとに主担当を変えて行うことですべてのメンバーが全体を把握し進めていますが、メンバーの変更が多いとこのやり方だけではうまくいかなくなってしまいます。
テスト計画の際に使用しているテストマップと呼ばれる、機能一覧に対するテスト実施を管理する表の中に自動テストがカバーしている点を記載する等行っていましたが、規模が小さい開発では更新されないこともあり、うまくドキュメント化出来ていないのが現状です。

## おわりに

まだまだ課題も多いですが、弊チームではこれからも自動化を駆使して高い品質の製品をなるべく早くお客様の元へお届けできるように尽力します！