この記事は、ARCTOUCH のブログ記事「Using Moq for unit testing with Prism EventAggregator」を個人的に雑訳したものです。
前回記事では、Publisher-Subscriber パターンと EventAggregator を使用するメリット、そして、プロジェクトでの使用方法についてを説明しました。さて、EventAggregator をコード上で使用する方法を学んだので、ユニットテスト(単体テスト)を書いてみましょう。ユニットテストは、高品質なソフトウェアをリリースする上で重要になります。(なんで、)EventAggregator をどのように使用したのかをテストする必要があります。しかし、これにはいくつかの課題があります。テストをするべき最も重要なシナリオは、つぎの2つです:
- Publisher(パブリッシャー)
イベントが正しいパラメーターで正常に発行されたかどうかを確認します(該当するなら) - Subscriber(サブスクライバー、購読者)
受信したイベントが正しく処理されたかどうかを確認する
これら(パブリッシャーとサブスクライバー)のテストをするために、Prism EventAggregator
を使ったコード実装を(本番環境でも、テストコードであっても)利用することができると思います。EventAggregator
自体は依存関係がほとんどないため、おそらくうまくテストできるはずです……が、そのようにするべきではありません。
ユニットテストでは、テストするコードをあらゆる依存関係から分離したいためです。
(本番環境でも、テストコードであっても)EventAggregator
を使ってしまうと、テストがユニットテストではなくて、統合テスト (integration test) のようになってしまいます。そこで、モックされた IEventAggregator
を使うことにします。
ここでは Moq Library を使用します。
Moq を使ったテストのやり方
まず、EventAggregator
の例を設定しましょう。イベント、パブリッシャー、サブスクライバーから始めます。
簡単なパラメーター <string>
を持ちます(イベント):
public class MyTextEvent : PubSubEvent<string> { }
ユーザーがボタンを押下したときにイベントを発行します(パブリッシャー):
public class MyPublisherViewModel { private readonly IEventAggregator _eventAggregator; public MyPublisherViewModel(IEventAggregator eventAggregator) { _eventAggregator = eventAggregator; OnButtonClickCommand = new DelegateCommand(PublishTextEvent); } // ... private void PublishTextEvent() { _eventAggregator .GetEvent<MyTextEvent>() .Publish("text to send"); } }
受信したテキストの値をパブリックなプロパティに設定します(サブスクライバー):
public class MySubscriberViewModel { private readonly IEventAggregator _eventAggregator; public MySubscriberViewModel(IEventAggregator eventAggregator) { _eventAggregator = eventAggregator; _eventAggregator .GetEvent<MyTextEvent>() .Subscribe(HandleMyTextEvent); } public string Text { get; set; } private void HandleMyTextEvent(string text) { Text = text; } }
パブリッシャーのテスト
一番簡単なテストから始めましょう。ユーザーがボタンをクリックしたときに、イベントが正しいテキストでパブリッシュされるかどうかを検証します。つまりテストは以下のような構造になります:
- Arrange
必要なモックをセットアップしてPublisherViewModel
をインスタンス化します。 - Act
OnButtonClickedCommand
を実行する - Assert
Publish(“text to send”)
が一度だけ呼ばれたことを確認する
まず、IEventAggregator
のモックを ViewModel のコンストラクターに注入する必要があります:
var eventAggregatorMock = new Mock<IEventAggregator>(); var viewModel = new MyPublisherViewModel(eventAggregatorMock.object);
設定するメソッドは、GetEvent<MyTextEvent>()
メソッドだけです。これは MyTextEvent
イベントのインスタンスを返却する必要があります。このうえで、なんらかを検証することになるので、イベントもモックになります:
var mockedEvent = new Mock<MyTextEvent>();
eventAggregatorMock
.Setup(x => x.GetEvent<MyTextEvent>())
.Returns(mockedEvent.Object);
最後に、Publish()
が正しい引数で呼び出されているかを確認しましょう。完成したテストは次のようになります:
// Arrange var mockedEvent = new Mock<MyTextEvent>(); var eventAggregatorMock = new Mock<IEventAggregator>(); eventAggregatorMock .Setup(x => x.GetEvent<MyTextEvent>()) .Returns(mockedEvent.Object); var viewModel = new MyPublisherViewModel(eventAggregatorMock.object); // Act viewModel.OnButtonClickCommand.Execute() // Assert mockedEvent.Verify(x => x.Publish("text to send"), Times.Once);
最初のユニットテストができました。さて、次のテストに進みましょう。
サブスクライバーのテスト
サブスクライバーのテストはすこし難しいです。似たような構成でやってみます:
- Arrange
必要なモックをセットアップして、SubscriberViewModel
をインスタンス化する - Act
イベントを処理するコードを実行する - Assert
Text プロパティに期待する値を持つことを確認する
ここで面倒になるのは、Act の段階です。イベントを実行するためのコードは HandleMyTextEvent(string text)
です。このメソッドは private なので、直接呼び出すことができないのです。
テストのためにアクセス修飾子を変更してはいけないため、このメソッドを public にすべきではありません。なので、なにか別のアプローチを考えないといけません。
なんとかして、private メソッドへ参照することで、マニュアルで呼び出すことが可能になります。幸いなことに、Moq
テストはこのやり方を可能にする便利なメソッドが用意されています。
それは Callback()
メソッドです。
上のテストと同じように IEventAggregator
の Callback()
と、MyTextEvent
の Callback()
のモックを作成し、GetEvent<MyTextEvent>()
メソッドを設定します。
つぎに、MyTextEvent.Subscribe(Action eventHandler)
を設定します。Returns<T>(T value)
を呼び出す代わりに Callback<T>(Action<T> callbackAction)
を呼び出します。
コールバックメソッドは、設定されているメソッドが呼び出されるたびに Action
を受け取ります。もし、設定されているメソッドがなにか引数を受け取った場合は、それらも Action
に渡されます。
今回の例では、Subscribe()
が Action<Action>
という引数を受け取るため、コールバックのアクションは Action
になります。言い換えると、コールバックのアクションは SubscriberViewModel
のコンストラクターで Publish()
に渡された Action
への参照を受け取ることになります。
参照を保存しておけば、あとから起動することができるようになります。すこし複雑に聞こえますが、コードで説明します:
Action<string> eventHandlerAction; var mockedEvent = new Mock<MyTextEvent>(); mockedEvent .Setup(x => x.Subscribe(It.IsAny<Action<string>>())) // ここで渡されたアクションを変数に格納して、テストでアクセスできるようにする .Callback(action => eventHandlerAction = action);
残念ながら、このコードでは例外が発生します。Moq
は Subscribe(Action<string>action)
を設定できません。オプションのパラメーターをすべて使用して設定をする必要があります。
mockedEvent .Setup(x => x.Subscribe( It.IsAny<Action<string>>(), // The event handler It.IsAny<ThreadOption>(), // threadOption It.IsAny<bool>(), // keepSubscriberReferenceAlive It.IsAny<Predicate<string>>())) // filter // action の引数以外は興味ないので、捨てています .Callback((action, _, __, ___) => eventHandlerAction = action);
これでイベントへの参照ができたので、Act のフェースを完了させることができます。テストの完成形はこのようになります:
// Arrange Action<string> eventHandlerAction; var mockedEvent = new Mock<MyTextEvent>(); mockedEvent .Setup(x => x.Subscribe( It.IsAny<Action<string>>(), // The event handler It.IsAny<ThreadOption>(), // threadOption It.IsAny<bool>(), // keepSubscriberReferenceAlive It.IsAny<Predicate<string>>())) // filter .Callback((action, _, __, ___) => eventHandlerAction = action); var eventAggregatorMock = new Mock<IEventAggregator>(); eventAggregatorMock .Setup(x => x.GetEvent<MyTextEvent>()) .Returns(mockedEvent.Object); var viewModel = new MyPublisherViewModel(eventAggregatorMock.object); // Act eventHandlerAction.Invoke("expected"); // Assert myTextEvent.Equal("expected", viewModel.Text);
ようやくサブスクライバーのユニットテストが完成しました。しかし、これはコードの量が多いです。もっとエレガントなやり方があるはずです。
そこで、EventAggregator_Mocker
が登場します。
Nuget パッケージ EventAggregator_Mocker
リファクタリングという魔法と拡張メソッドという神秘を利用して、私はこの nuget パッケージを作成しました。
なんで、このパッケージは ARCTOUCH の人が作ったパッケージになります。GitHub の公開もありますが、そこまでメジャーなパッケージではないです。雑訳のレベルも翻訳ツールを利用して加速させました。
このパッケージには Mock<IEventAggregator>
の2つの拡張メソッドが含まれています:
- パラメーターのないイベントのモック用
Mock<TEvent> RegisterNewMockedEvent<TEvent>(Action<Action>onSubscribeAction = null)
- パラメーターのあるイベントのモック用
Mock<TEvent> RegisterNewMockedEvent<TEvent, TParam>(Action<Action<TParam>> onSubscribeAction = null)
これらは、いずれもモックされた IEventAggragator
に登録されている(モックされた)イベントを返却します。また、これらはそれぞれコールバックのアクションとして使用されるオプションのパラメーター Action
を持っているため、イベントを処理するアクションへの参照を取得することができます。
このパッケージを使用すると、上記のユニットテストはつぎのようになります:
Publisher:
// Arrange var eventAggregatorMock = new Mock<IEventAggregator>(); // イベントハンドラへ参照する必要はないので、Action を渡しません Mock<MyTextEvent> mockedEvent = eventAggregatorMock.RegisterNewMockedEvent(); var viewModel = new MyPublisherViewModel(eventAggregatorMock.object); // Act viewModel.OnButtonClickCommand.Execute(); // Assert mockedEvent.Verify(x => x.Publish("text to send"), Times.Once);
Subscriber:
// Arrange Action<string> eventHandlerAction; var eventAggregatorMock = new Mock<IEventAggregator>(); // // イベントは必要ないので、戻り値を破棄しています _ = eventAggregatorMock.RegisterNewMockedEvent<MyTextEvent>(action => eventHandlerAction = action); var viewModel = new MyPublisherViewModel(eventAggregatorMock.object); // Act eventHandlerAction.Invoke("expected"); // Assert myTextEvent.Equal("expected", viewModel.Text);
コードブロックは、前より短く読みやすいものになりました。パッケージのソースコードは「ここ」にあります。実際に動作させているサンプルは「ここ」。
今回の Prism EventAggregator
シリーズがお役に立てれば幸いです。