sh1’s diary

プログラミング、読んだ本、資格試験、ゲームとか私を記録するところ

Prism EventAggregator はユニットテストに Moq を使うこと

f:id:shikaku_sh:20211013180838p:plain:w400

この記事は、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() メソッドです。

上のテストと同じように IEventAggregatorCallback() と、MyTextEventCallback() のモックを作成し、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);

残念ながら、このコードでは例外が発生します。MoqSubscribe(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 シリーズがお役に立てれば幸いです。

参考