7

Moqは、私の最新のプロジェクトに少し夢中になっています。最近バージョン4.0.10827にアップグレードしましたが、新しい動作のように見えることに気づきました。

基本的に、テストしているコードでモック関数(MakeCallこの例では)を呼び出すと、オブジェクト(TestClass)を渡します。私がテストしているコードはTestClass、の呼び出しの前後にオブジェクトに変更を加えますMakeCall。コードが完成したら、MoqのVerify関数を呼び出します。私の期待は、MoqがMakeCall、おそらくディープクローニングのようなメカニズムを介して、私が渡した完全なオブジェクトを記録することです。MakeCallこのようにして、呼び出されることを期待している正確なオブジェクトで呼び出されたことを確認できます。残念ながら、これは私が見ているものではありません。

私はこれを以下のコードで説明しようとしています(うまくいけば、プロセスの中で少し明確にします)。

  1. まず、新しいTestClassオブジェクトを作成します。そのVarプロパティはに設定され"one"ます。
  2. mockedObject次に、テスト対象である モックオブジェクトを作成します。
  3. MakeCall次に、のメソッドを呼び出しますmockedObject(ちなみに、例で使用されているMachine.Specificationsフレームワークでは、When_Testingクラス内のコードを上から下に読み取ることができます)。
  4. TestClass次に、モックされたオブジェクトをテストして、Var値が。で実際に呼び出されたことを確認します"one"。予想通り、これは成功します。
  5. 次に、プロパティをにTestClass再割り当てして、元のオブジェクトに変更を加えます。 Var"two"
  6. 次に、Moqが値が.のaでMakeCall呼び出されたとまだ考えているかどうかを確認します。私はそれが真実であると期待していますが、これは失敗します。 TestClass"one"
  7. 最後に、MoqMakeCallが実際TestClassに値が。のオブジェクトによって呼び出されたと見なすかどうかをテストします"two"。これは成功しますが、最初は失敗すると予想していました。

Moqが元のTestClassオブジェクトへの参照のみを保持していることは明らかであり、その値を免責で変更することができ、テストの結果に悪影響を及ぼします。

テストコードに関するいくつかの注意事項。 IMyMockedInterface私が嘲笑しているインターフェースです。 は私がメソッドTestClassに渡しているクラスであるため、私が抱えている問題を示すために使用しています。MakeCall最後にWhen_Testing、テストコードを含む実際のテストクラスです。Machine.Specificationsフレームワークを使用しているため、奇妙な項目がいくつかあります('Because of'、'It should ...')。これらは、テストを実行するためにフレームワークによって呼び出される単なるデリゲートです。それらは簡単に削除でき、必要に応じて、含まれているコードを標準関数に配置する必要があります。すべてを許可するので、この形式のままにしましたValidate完了するための呼び出し(「アレンジ、アサーションの実行」パラダイムと比較して)。明確にするために、以下のコードは私が問題を抱えている実際のコードではありません。私はこれと同じ振る舞いを複数の場所で見たので、単に問題を説明することを目的としています。

using Machine.Specifications;
// Moq has a conflict with MSpec as they both have an 'It' object.
using moq = Moq;

public interface IMyMockedInterface
{
    int MakeCall(TestClass obj);
}

public class TestClass
{
    public string Var { get; set; }

    // Must override Equals so Moq treats two objects with the 
    // same value as equal (instead of comparing references).
    public override bool Equals(object obj)
    {
        if ((obj != null) && (obj.GetType() != this.GetType()))
            return false;
        TestClass t = obj as TestClass;
        if (t.Var != this.Var)
            return false;
        return true;
    }

    public override int GetHashCode()
    {
        int hash = 41;
        int factor = 23;
        hash = (hash ^ factor) * Var.GetHashCode();
        return hash;
    }

    public override string ToString()
    {
        return MvcTemplateApp.Utilities.ClassEnhancementUtilities.ObjectToString(this);
    }
}

[Subject(typeof(object))]
public class When_Testing
{
    // TestClass is set up to contain a value of 'one'
    protected static TestClass t = new TestClass() { Var = "one" };
    protected static moq.Mock<IMyMockedInterface> mockedObject = new moq.Mock<IMyMockedInterface>();
    Because of = () =>
    {
        mockedObject.Object.MakeCall(t);
    };

    // Test One
    // Expected:  Moq should verify that MakeCall was called with a TestClass with a value of 'one'.
    // Actual:  Moq does verify that MakeCall was called with a TestClass with a value of 'one'.
    // Result:  This is correct.
    It should_verify_that_make_call_was_called_with_a_value_of_one = () =>
        mockedObject.Verify(o => o.MakeCall(new TestClass() { Var = "one" }), moq.Times.Once());

    // Update the original object to contain a new value.
    It should_update_the_test_class_value_to_two = () =>
        t.Var = "two";

    // Test Two
    // Expected:  Moq should verify that MakeCall was called with a TestClass with a value of 'one'.
    // Actual:  The Verify call fails, claiming that MakeCall was never called with a TestClass instance with a value of 'one'.
    // Result:  This is incorrect.
    It should_verify_that_make_call_was_called_with_a_class_containing_a_value_of_one = () =>
        mockedObject.Verify(o => o.MakeCall(new TestClass() { Var = "one" }), moq.Times.Once());

    // Test Three
    // Expected:  Moq should fail to verify that MakeCall was called with a TestClass with a value of 'two'.
    // Actual:  Moq actually does verify that MakeCall was called with a TestClass with a value of 'two'.
    // Result:  This is incorrect.
    It should_fail_to_verify_that_make_call_was_called_with_a_class_containing_a_value_of_two = () =>
        mockedObject.Verify(o => o.MakeCall(new TestClass() { Var = "two" }), moq.Times.Once());
}

これに関していくつか質問があります:

これは予想される動作ですか?
これは新しい動作ですか?
私が気付いていない回避策はありますか?
ベリファイを間違って使用していますか?
この状況を回避するためにMoqを使用するより良い方法はありますか?

何卒よろしくお願い申し上げます。

編集:
これは私がこの問題を経験した実際のテストとSUTコードの1つです。うまくいけば、それは説明として機能します。

// This is the MVC Controller Action that I am testing.  Note that it 
// makes changes to the 'searchProjects' object before and after 
// calling 'repository.SearchProjects'.
[HttpGet]
public ActionResult List(int? page, [Bind(Include = "Page, SearchType, SearchText, BeginDate, EndDate")] 
    SearchProjects searchProjects)
{
    int itemCount;
    searchProjects.ItemsPerPage = profile.ItemsPerPage;
    searchProjects.Projects = repository.SearchProjects(searchProjects, 
        profile.UserKey, out itemCount);
    searchProjects.TotalItems = itemCount;
    return View(searchProjects);
}


// This is my test class for the controller's List action.  The controller 
// is instantiated in an Establish delegate in the 'with_project_controller' 
// class, along with the SearchProjectsRequest, SearchProjectsRepositoryGet, 
// and SearchProjectsResultGet objects which are defined below.
[Subject(typeof(ProjectController))]
public class When_the_project_list_method_is_called_via_a_get_request
    : with_project_controller
{
    protected static int itemCount;
    protected static ViewResult result;
    Because of = () =>
        result = controller.List(s.Page, s.SearchProjectsRequest) as ViewResult;

    // This test fails, as it is expecting the 'SearchProjects' object 
    // to contain:
    // Page, SearchType, SearchText, BeginDate, EndDate and ItemsPerPage
    It should_call_the_search_projects_repository_method = () =>
        s.Repository.Verify(r => r.SearchProjects(s.SearchProjectsRepositoryGet, 
            s.UserKey, out itemCount), moq.Times.Once());

    // This test succeeds, as it is expecting the 'SearchProjects' object 
    // to contain:
    // Page, SearchType, SearchText, BeginDate, EndDate, ItemsPerPage, 
    // Projects and TotalItems
    It should_call_the_search_projects_repository_method = () =>
        s.Repository.Verify(r => r.SearchProjects(s.SearchProjectsResultGet, 
            s.UserKey, out itemCount), moq.Times.Once());

    It should_return_the_correct_view_name = () =>
        result.ViewName.ShouldBeEmpty();

    It should_return_the_correct_view_model = () =>
        result.Model.ShouldEqual(s.SearchProjectsResultGet);
}


/////////////////////////////////////////////////////
// Here are the values of the three test objects
/////////////////////////////////////////////////////

// This is the object that is returned by the client.
SearchProjects SearchProjectsRequest = new SearchProjects()
{
    SearchType = SearchTypes.ProjectName,
    SearchText = GetProjectRequest().Name,
    Page = Page
};

// This is the object I am expecting the repository method to be called with.
SearchProjects SearchProjectsRepositoryGet = new SearchProjects()
{
    SearchType = SearchTypes.ProjectName,
    SearchText = GetProjectRequest().Name,
    Page = Page, 
    ItemsPerPage = ItemsPerPage
};

// This is the complete object I expect to be returned to the view.
SearchProjects SearchProjectsResultGet = new SearchProjects()
{
    SearchType = SearchTypes.ProjectName,
    SearchText = GetProjectRequest().Name,
    Page = Page, 
    ItemsPerPage = ItemsPerPage,
    Projects = new List<Project>() { GetProjectRequest() },
    TotalItems = TotalItems
};
4

2 に答える 2

3

最終的に、あなたの質問は、モックフレームワークがモックと対話するときに使用するパラメーターのスナップショットを取得して、パラメーターが検証のポイント。

これは論理的な観点からは合理的な期待だと思います。値YでアクションXを実行しています。モックに「値YでアクションXを実行しましたか」と尋ねると、システムの現在の状態に関係なく、「はい」と表示されるはずです。

発生している問題を要約すると、次のようになります。


  • 最初に、参照型パラメーターを使用してモックオブジェクトのメソッドを呼び出します。

  • Moqは、渡された参照タイプパラメータとともに呼び出しに関する情報を保存します。

  • 次に、渡した参照と等しいオブジェクトを使用してメソッドが1回呼び出されたかどうかをMoqに問い合わせます。

  • Moqは、指定されたパラメーターと一致するパラメーターを使用してそのメソッドの呼び出しの履歴をチェックし、「はい」と答えます。

  • 次に、モックのメソッド呼び出しにパラメーターとして渡したオブジェクトを変更します。

  • Moqが履歴に保持している参照メモリスペースは、新しい値に変更されます。

  • 次に、Moqに、保持している参照と等しくないオブジェクトを使用してメソッドが1回呼び出されたかどうかを尋ねます。

  • Mockは、指定されたパラメーターと一致するパラメーターを使用してそのメソッドへの呼び出しの履歴をチェックし、noを報告します。


特定の質問に答えるには:

  1. これは予想される動作ですか?

    私はノーと言うでしょう。

  2. これは新しい動作ですか?

    わかりませんが、プロジェクトがこれを容易にする動作を一度に持っていて、後でモックごとに1つの使用法のみを検証するという単純なシナリオのみを許可するように変更されたのではないかと疑っています。

  3. 私が気付いていない回避策はありますか?

    この2つの方法で答えます。

    技術的な観点から、回避策はモックではなくテストスパイを使用することです。Test Spyを使用すると、渡された値を記録し、ディープクローンの実行、オブジェクトのシリアル化、後で比較したい特定の値の保存など、状態を記憶するための独自の戦略を使用できます。

    テストの観点から、「正面玄関を最初に使用する」という原則に従うことをお勧めします。。状態ベースのテストと相互作用ベースのテストの時間があると思いますが、相互作用がシナリオの重要な部分でない限り、実装の詳細に自分自身を結び付けないようにする必要があります。場合によっては、関心のあるシナリオは主にインタラクション(「アカウント間での資金の移動」)に関するものですが、それ以外の場合、本当に気になるのは正しい結果を得る(「$ 10を引き出す」)ことだけです。コントローラの仕様の場合、これはコマンドカテゴリではなくクエリカテゴリに分類されるようです。それらが正しい限り、あなたはそれがあなたが望む結果をどのように得るかを本当に気にしません。したがって、この場合は状態ベースのテストを使用することをお勧めします。別の仕様がシステムに対するコマンドの発行に関するものである場合、それでも、最初に使用することを検討する必要があるフロントドアソリューションが存在する可能性がありますが、相互作用ベースのテストを実行することが必要または重要な場合があります。ただ私の考えです。

  4. ベリファイを間違って使用していますか?

    あなたはVerify()メソッドを正しく使用しています、それはあなたがそれを使用しているシナリオをサポートしていないだけです。

  5. この状況を回避するためにMoqを使用するより良い方法はありますか?

    現在、このシナリオを処理するためにMoqが実装されているとは思いません。

お役に立てれば、

デレクグリア
http
: //derekgreer.lostechies.comhttp:
//aspiringcraftsman.com@derekgreer

于 2011-04-09T00:11:53.713 に答える
0

まず、MoqMSpecを宣言することで、との間の競合を回避できます。

using Machine.Specifications;
using Moq;
using It = Machine.Specifications.It;

次に、たとえば、Moq.Moqを使用する場合にのみ接頭辞を付ける必要があります。ItMoq.It.IsAny<>()


あなたの質問に。

注:これは元の回答ではありませんが、OPが質問に実際のサンプルコードを追加した後に編集された回答です

私はあなたのサンプルコードを試してきましたが、それはMoqよりもMSpecに関係していると思います。どうやら(そして私もこれを知りませんでした)、Itデリゲート内のSUT(テスト対象システム)の状態を変更すると、変更が記憶されます。現在起こっていることは次のとおりです。

  1. Becauseデリゲートが実行されます
  2. Itデリゲートは次々に実行されます。状態を変更した場合、次の It ユーザーはの設定を確認できません Because。したがって、テストに失敗しました。

私はあなたのスペックをSetupForEachSpecificationAttribute:でマークしてみました

[Subject(typeof(object)), SetupForEachSpecification]
public class When_Testing
{
    // Something, Something, something... 
}

属性はその名前が示すように機能します:それはあなたEstablishBecause すべての前に It実行されます。属性を追加すると、仕様は期待どおりに動作しました。3回成功、1回失敗(Var = "two"での検証)。

SetupForEachSpecificationAttributeあなたの問題を解決しますか、それともあなたのテストに受け入れられないたびにリセットしItますか?

参考:私は使用Moq v4.0.10827.0していますMSpec v0.4.9.0


無料のヒント#2:Mspecを使用してASP.NET MVCアプリをテストしている場合は、MVC用のJamesBroomeのMSpec拡張機能を確認することをお勧めします。

于 2011-04-04T09:14:28.170 に答える