ヘキサゴナルアーキテクチャの基本構造と単体・結合テスト(単体テストの考え方/使い方の読書メモ)
単体テストの考え方/使い方を読んだ。タイトルが単体テストだが、手動テスト・結合テスト・E2Eテストまで含めたテスト全般に適用できる普遍的な考え方を開発者目線で述べており、何をどうテストすべきか、テストすべきでないか、モックをどこに使うかといった判断を的確に行えるようになる。その考え方をベースにアプリケーションの設計論にまで踏み込んで解説しており、現実の開発への適用をイメージしやすかった。テスト駆動開発のような特定の方法論を展開するものではないので、あらゆる開発スタイルの人に読んでほしい本。ヘキサゴナルアーキテクチャやドメイン駆動設計の基本的な知識があると読みやすいと思う。
以下勉強メモ。
- アプリケーションサービス層とドメイン層に明確に分離する。
- 観測できる境界における入出力をテストする。中身はブラックボックスとして扱う。つまり処理結果の確認で直接DB等の内部状態を見たりしない。これによりリファクタリング耐性を確保する。
- 結合テストやE2Eテストは、単体テストよりもリファクタリング耐性・退行に対する保護が強いので、テストの実行時間が許容できる範囲で充実させる。ドメインロジックは単体テストで検証することで実行時間を抑えることができる。極端に言えば、実行時間が十分に短いならアプリ全体をブラックボックスとして全テストケースをE2Eテストとすればよい。
似たような話は他の本でも触れているはずだが結構忘れているのと、テストという観点から納得感のある解説がされていて腑に落ちたのでよかった。
自社ではテストケースのドキュメントを書かないといけないルールだが、このアプローチと整合させるにはどうするのがいいか。ドキュメントを考える前に、そもそも機能要件・不具合修正内容に対して十分テストできているのかをどうレビューし記録するのが良いのだろう。コードメトリクスで保証できるものではないので、機能要件・不具合修正内容に対するテストケースとして入力と出力の一覧を書き出して、これをレビューことになるだろう。手動テストであってもここに手順を入れない(入れるとしてもメモ程度)ことで機能レベルのリファクタリング耐性を確保し作業効率の低下も防ぐ。次にこのテストケースを結合テストまたは単体テストとして実装できるなら実装し、例えばテスト名で紐づける。
しかしこれだと二重管理になっている。そもそもドキュメントは開発者以外でも読めて、開発者も非開発者も俯瞰しやすくレビューできるようにすることが目的なので、手動テストと自動テストで分離し、手動テストはドキュメントを普通に手書きする一方、自動テストについてはテストの実装からドキュメントを生成するのが良いだろう。生成するツールが見当たらないので自作するしかないか…。
OPC UAサーバを実装してみた
これの続き。チャットサーバとして、OPC UAサーバのサンプル実装をした。その際UA-.NETStandardのサンプルや各所に散らばっている資料を理解する必要があったので、それらのメモを含めて実装ガイドとしてQiitaに記事を公開した。
gRPCのprotoファイルからOPC UAサーバのデザインXMLを生成できると良いかもと思ったのでそれも作った。
以下雑感。
OPC UAはかなり色々なことができる仕組みなので、公式サンプルには色々な実装が含まれていたり、組み込み機器のインターフェースっぽい雰囲気になるよう意識しているのかサンプルとしては冗長な部分も多かった。私としてはRPCサーバ的な実装をしたかったので、それに必要な部分だけエッセンスを抽出してサンプルを書いたところ、シンプルにそぎ落とせたと思う。
UA-.NETStandardを使ってサーバ実装した感想としては、すべてをノードで扱うにあたってなんというか全部手書きする感覚で、やはりgRPCサーバのようなサクサク作れる感はまったくなかった。とはいえ、ちょっとしたものを作るのであれば有償SDKを使うまでもなさそう。
私が関わっているプロジェクトでgRPCとOPC UAのどっちでサーバを用意する?という話が出た時には、選べるならgRPCにしましょうということで進めた。OPC UAじゃないとダメなプロジェクトが出てきたときに改めて製品開発レベルの知見が得られればまとめたいと思う。
イベントハンドラで同期・非同期処理をする実装パターン
イベント・イベントハンドラであっても、asyncメソッドの実装時と考え方は同じだが、イベントという皮を被ると少しわかりにくくなるのでまとめておく。
【Case 1】イベントソースは、イベントハンドラの完了を待機して抜けたい
# | awaitしたい? | イベントソース側 | イベントハンドラ側 |
---|---|---|---|
1-1 | - | イベントをInvoke | 同期処理する |
1-2 | 〇 | 非同期イベントをawait Invokeして完了待機 | async Taskメソッドにする |
1-2ではイベントハンドラが複数登録されていた場合、awaitのタイミングで後続のイベントハンドラが走り始めるので並列になる。工夫すれば逐次実行にもできる。実装例は以下を参照。
【Case 2】イベントソースは、イベントハンドラの処理が終わる前に抜けたい。イベントが連続的に発生する状況でイベントハンドラの処理順の保証は不要
# | awaitしたい? | イベントソース側 | イベントハンドラ側 |
---|---|---|---|
2-1 | - | イベントのInvokeをTask.Runでくるむ | 同期処理する |
2-2 | 〇 | イベントをInvoke | async voidメソッドにする |
2-1ではイベントハンドラの逐次呼び出しをTask.Runで包んでいるので、イベントハンドラの処理が並列にならないことに注意。
2-2ではイベントハンドラが複数登録されていた場合、awaitのタイミングで後続のイベントハンドラが走り始めるので並列になる。
【Case 3】イベントソースは、イベントハンドラの処理が終わる前に抜けたい。イベントが連続的に発生する状況でイベントハンドラの処理順を保証したい
# | awaitしたい? | イベントソース側 | イベントハンドラ側 |
---|---|---|---|
3 | -/〇 | イベントをInvoke | BlockingCollectionを使用したプロデューサーコンシューマーパターンで別スレッドへキューイングして処理するようにして、ハンドラ自体はすぐに抜ける |
TFS+Visual Studioで勝手に文字コードが変換される問題
BOM無しのUTF-8で書かれた複数のソースファイルをVisual StudioからTFSにチェックインしていたが、これをエディタで開いたり別ブランチへマージしたりすると、一部のファイルだけなぜか勝手にShift_JISに変換されてしまい、UTF-8のつもりで扱うと文字化けしてしまうという問題が起きて調査した。
- TFSはエンコードを保持する
- Visual StudioはASCIIコードのみ含むBOM無しのファイルは日本語環境ならShift_JISと認識する
- 問題が起きたファイルは、初回チェックイン時にASCIIコードのみを含みTFSにShift_JISと登録されていたところにUTF-8の日本語を追加していた
- 問題が起きていなかった日本語を含むファイルは、初回チェックイン時から日本語を含んでいてTFS上でUTF-8と登録されていた
このことから、TFS上でShift_JISと誤認識された状態のファイルにUTF-8の2バイト文字を追加しても、TFS上のエンコードは更新されないということが分かり、そのファイルをVisual Studioのエディタで開いたりマージしたりすると自動的にShift_JISに変換されると思われた。TFSからファイルを取得しただけなら変換かからないし、変換されても文字の見た目は変化しないし警告の類も出ないのでとても気が付きにくい。
動作上の問題の他にも、ブラウザからTFS上のソースコード・変更セットを見た時もこのTFS上のエンコードが効くので、実際のエンコードとずれていると日本語コメントとかが文字化けしてしまう。
良い手が見つからなかったので、自動テストでTFSのコマンドを叩いて、指定ディレクトリ以下のソースコードについて、エンコードをチェックするようにして、チェックイン後にもしShift_JISが含まれていたら検出できるようにした。
他には、例えば常に初回のチェックイン時から日本語やおまじないコメントを入れておくような対策を取るか
UTF-8 の文字化け対策! 「美乳」ではなく「†(ダガー)」を使う | 亜細亜ノ蛾
そもそもこのファイルはVisual Studio Code書いたものだったので、そっちから直接チェックインすれば、Visual Studio Code自身は.editorconfigでBOM無しUTF-8だと分かっているのでうまくいくのかもしれない。未確認。
ジェネリックメソッドで値型を返す時にボックス化させない方法
戻り値の型がTのジェネリックメソッドを実装した時、return (T)(object)value
のようなキャストを書いてしまうと、値型ではボックス化⇒ボックス化解除が行われてしまう。そもそもこういうコードを書くならジェネリクスじゃないだろという話は置いといて、ボックス化させない面白い方法を知った。
c# - How to avoid boxing of value types - Stack Overflow
c# - Primitive type conversion in generic method without boxing - Stack Overflow
ポイントは、値そのものではなくFunc
でキャストすること。Func<int>
⇒ object
⇒ Func<T>
とキャストすればコンパイルは通るし、実行時にT
がint
なら動作する。なお、Func<int>
⇒ object
⇒ Func<object>
やFunc<int>
⇒ object
⇒ Func<double>
などは実行時に例外となる。
Windows 10アップデートに失敗したらインプレースアップグレードを試す
Windows 10のバージョン1909で、Windows Updateで届いた2004へのアップデートをしようとしたところ、再起動が何度かかかった後にブルースクリーン(page fault in nonpaged area)になり、電源再投入すると復旧されて1909のまま、という状態になった。1か月置いて再度やってみたり、Cドライブの空き容量を増やしてみたりするも同じ結果。
そうこうしてるうちにバージョン20H2を適用しないかという表示が出るようになったので試すも、今度は事前に適用しろと言われた更新プログラムが以下のエラーで失敗。
更新プログラムのインストール中に問題が発生しましたが、後で再試行されます。- 0x80073701
(自作PCなので)マザーボードメーカーやグラボメーカーのサイトから最新ドライバ類を入れてみたりしている間に、エラーコードでググってこちらのページを発見。インプレースアップグレードなる方法があることを知る。
エラーコード0x80073701 windows updateの更新に失敗する - マイクロソフト コミュニティ
やり方:
Windows 10 でインプレース アップグレードを実行する方法 - マイクロソフト コミュニティ
アップデートを1回試行するたびにかなりの時間がかかっていてもう面倒だったので、バージョン2004は飛ばすことになるが、バージョン20H2でインプレースアップグレードしたところ成功した。
Dictionaryの仕組みとGetHashCode
Dictionary<TKey, TValue>へ入れたり取り出したりするとき、こんな感じになっている。
もしkey1.GetHashCode()の返すハッシュが途中で変わってしまった場合、値の入ったバケツが見つからなくなってしまう。そうならないよう、GetHashCode()はインスタンスの存続する限り同じ値を返さなければならないことが分かる。
もう1点、同じハッシュを返すキーが多いとEqualsでなめる回数が増えてしまう。そうならないよう、GetHashCode()は十分に分散したハッシュを返すべきであることが分かる。
デフォルトのGetHashCode()の実装では、インスタンスごとに固有の適当な値が返るようになっているため、これら両方を満たす。しかし、クラスをキーとする場合、参照の等価性により引き当てられるため、辞書に入れた時のキーのインスタンスそのものをキーとしないとアクセスできない。これを値の同値性で引き当てたい場合、同値と判定されるインスタンス同士が同じハッシュを返す必要があり、(IEquatableの実装に加えて)適切な実装でGetHashCode()をoverrideする必要がある。詳しくは下記の記事参照。