MEFでImportしているメンバをモックに差し替える

github.com

C#でMEF使っているコードで、ユニットテストでMoqを使ってモックに差し替える話。

[Export]
public class Test1
{
    [Import]
    private Test2 m_test2;

    public Test1()
    {
    }
}

[Export]
public class Test2
{
    [Import]
    private Test1 m_test1;

    public Test2()
    {
    }
}

class MyClass
{
    [Import]
    private Test1 m_test1;

    [Import]
    private Test2 m_test2;

    // コンストラクタとかでインポートを実行
}

こういうコードがあった。Test1とTest2が相互に参照している。これの動作自体は問題ない。 Test1のコンストラクタ⇒Test2のコンストラクタ⇒両方のImportが解決という流れになる。

しかしユニットテストでTest1とTest2をモックに差し替えようとすると簡単にはいかない。 Mockクラスは内部的に継承でモックを実現しているのでprivateメンバにアクセスすることはできない。

// NG
new PrivateObject(test1Mock.Object).SetField("m_test2", test2Mock.Object);

例えばこのように書いてもTest1ではなくTest1を継承したMockのprivateメンバにアクセスしてしまい、m_test2が存在しないのでエラーになる。

解決策は以下のようにprotected virtualなプロパティにして、Mockの機能でprotectedなメンバにアクセスして差し替えるしかなさそうだ。


2019/1/22追記 PrivateObjectExtensionsを使えば何も気にせずm_test2へアクセスできます。

cactuaroid.hatenablog.com

PrivateObjectであってもコンストラクタに型を指定するオーバーロードがあり、それを使えば基底クラスのメンバにもアクセスできます。


テストのためにアクセシビリティを変えるのは気が進まないのだがMoqを使っている以上どうしようもないので受け入れるしかない。

[Export]
public class Test1
{
    [Import]
    protected virtual Test2 Test2 { get; set; }

    public Test1()
    {
    }
}

[Export]
public class Test2
{
    [Import]
    protected virtual Test1 Test1 { get; set; }

    public Test2()
    {
    }
}
var test1Mock = new Mock<Test1>();
var test2Mock = new Mock<Test2>();

test1Mock.Protected().Setup("Test2").Return(test2Mock.Object);
test1Mock.Protected().Setup("Test1").Return(test1Mock.Object);

var myClass = new MyClass();
new PrivateObject(myClass).SetField("m_test1", test1Mock.Object);
new PrivateObject(myClass).SetField("m_test2", test2Mock.Object);

MyClassの方は本物なので普通にPrivateObjectで差し替えれば良い。 ※上記QuickstartのMiscellaneousを参照。

MEFが持っているインスタンスを差し替えてしまってインポートさせれば…とも考えたが、インスタンスの管理を隠ぺいして任せる仕組みがMEFなわけで、ユニットテストで自前のモックのインスタンスを使わせる行為とは相反する。実際MEFが管理しているインスタンスを差し替えるようなことはできない。ユニットテストでは無理にMEFを使おうとしない方がよい。

他には[ImportingConstructor]を使うとモックしやすいし依存関係を満たさずに作成できないから分かりやすいという情報が出てきた。

// NG
[Export]
public class Test1
{
    private Test2 m_test2;

    [ImportingConstructor]
    public Test1(Test2 test2)
    {
        m_test2 = test2;
    }
}

[Export]
public class Test2
{
    private Test1 m_test1;

    [ImportingConstructor]
    public Test2(Test1 test1)
    {
        m_test1 = test1;
    }
}

しかしこれだと動かない。考えてみれば当たり前だがコンストラクタ同士でデッドロック状態になっていてMEFが解決できず両方のコンストラクタは呼ばれることはなかった。

というわけで、こういう相互に参照しあっているような場合は[Import]を使うようにして、ユニットテストでモックに差し替えるならprotected virtualなプロパティにしてしまいMock.Protected()でアクセスして差し替えるという結論。