ヘキサゴナルアーキテクチャの基本構造と単体・結合テスト(単体テストの考え方/使い方の読書メモ)

単体テストの考え方/使い方を読んだ。タイトルが単体テストだが、手動テスト・結合テスト・E2Eテストまで含めたテスト全般に適用できる普遍的な考え方を開発者目線で述べており、何をどうテストすべきか、テストすべきでないか、モックをどこに使うかといった判断を的確に行えるようになる。その考え方をベースにアプリケーションの設計論にまで踏み込んで解説しており、現実の開発への適用をイメージしやすかった。テスト駆動開発のような特定の方法論を展開するものではないので、あらゆる開発スタイルの人に読んでほしい本。ヘキサゴナルアーキテクチャドメイン駆動設計の基本的な知識があると読みやすいと思う。

以下勉強メモ。

  • アプリケーションサービス層とドメイン層に明確に分離する。
    • アプリケーションサービス層
      • ドメイン層のクラスをインスタンス化し呼び出すだけ。状態やドメインロジックを持たない。
      • プロセス外依存とドメイン層の間の仲介、例えばDBを読み取ってドメイン層に渡したり、ドメイン層からの出力をDBに書き込んだりはここの責務。
      • 結合テスト(統合テスト)で保護する。
      • プロセス外依存のうち、管理下にない依存のみモックに置き換えて、やり取りをテストする。管理下にある依存(自アプリしか使わないDBなど)は観測できる境界とはみなさずそのまま使ってテストを構築することでリファクタリング耐性を確保する。
    • ドメイン
      • プロセス外依存を持たない。
      • 単体テストで保護する。ただし単純でロジックを含まないコードは単体テストしない。
      • モックを使う必要がない。
    • 分離できていないと、過度に複雑で、カプセル化ができておらず関心事の流出したコードになり、適切なテストができなくなる。
  • 観測できる境界における入出力をテストする。中身はブラックボックスとして扱う。つまり処理結果の確認で直接DB等の内部状態を見たりしない。これによりリファクタリング耐性を確保する。
  • 結合テストやE2Eテストは、単体テストよりもリファクタリング耐性・退行に対する保護が強いので、テストの実行時間が許容できる範囲で充実させる。ドメインロジックは単体テストで検証することで実行時間を抑えることができる。極端に言えば、実行時間が十分に短いならアプリ全体をブラックボックスとして全テストケースをE2Eテストとすればよい。

似たような話は他の本でも触れているはずだが結構忘れているのと、テストという観点から納得感のある解説がされていて腑に落ちたのでよかった。

自社ではテストケースのドキュメントを書かないといけないルールだが、このアプローチと整合させるにはどうするのがいいか。ドキュメントを考える前に、そもそも機能要件・不具合修正内容に対して十分テストできているのかをどうレビューし記録するのが良いのだろう。コードメトリクスで保証できるものではないので、機能要件・不具合修正内容に対するテストケースとして入力と出力の一覧を書き出して、これをレビューことになるだろう。手動テストであってもここに手順を入れない(入れるとしてもメモ程度)ことで機能レベルのリファクタリング耐性を確保し作業効率の低下も防ぐ。次にこのテストケースを結合テストまたは単体テストとして実装できるなら実装し、例えばテスト名で紐づける。

しかしこれだと二重管理になっている。そもそもドキュメントは開発者以外でも読めて、開発者も非開発者も俯瞰しやすくレビューできるようにすることが目的なので、手動テストと自動テストで分離し、手動テストはドキュメントを普通に手書きする一方、自動テストについてはテストの実装からドキュメントを生成するのが良いだろう。生成するツールが見当たらないので自作するしかないか…。

OPC UAサーバを実装してみた

cactuaroid.hatenablog.com

これの続き。チャットサーバとして、OPC UAサーバのサンプル実装をした。その際UA-.NETStandardのサンプルや各所に散らばっている資料を理解する必要があったので、それらのメモを含めて実装ガイドとしてQiitaに記事を公開した。

github.com

qiita.com

gRPCのprotoファイルからOPC UAサーバのデザインXMLを生成できると良いかもと思ったのでそれも作った。

github.com

以下雑感。

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のタイミングで後続のイベントハンドラが走り始めるので並列になる。工夫すれば逐次実行にもできる。実装例は以下を参照。

cactuaroid.hatenablog.com

【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上で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>objectFunc<T>とキャストすればコンパイルは通るし、実行時にTintなら動作する。なお、Func<int>objectFunc<object>Func<int>objectFunc<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>へ入れたり取り出したりするとき、こんな感じになっている。

参考:https://referencesource.microsoft.com/#mscorlib/system/collections/generic/dictionary.cs,bcd13bb775d408f1

もしkey1.GetHashCode()の返すハッシュが途中で変わってしまった場合、値の入ったバケツが見つからなくなってしまう。そうならないよう、GetHashCode()はインスタンスの存続する限り同じ値を返さなければならないことが分かる。

もう1点、同じハッシュを返すキーが多いとEqualsでなめる回数が増えてしまう。そうならないよう、GetHashCode()は十分に分散したハッシュを返すべきであることが分かる。

デフォルトのGetHashCode()の実装では、インスタンスごとに固有の適当な値が返るようになっているため、これら両方を満たす。しかし、クラスをキーとする場合、参照の等価性により引き当てられるため、辞書に入れた時のキーのインスタンスそのものをキーとしないとアクセスできない。これを値の同値性で引き当てたい場合、同値と判定されるインスタンス同士が同じハッシュを返す必要があり、(IEquatableの実装に加えて)適切な実装でGetHashCode()をoverrideする必要がある。詳しくは下記の記事参照。

qiita.com