web-dev-qa-db-ja.com

Moqにrefまたはoutの引数を無視させる方法

RhinoMocksでは、モックに包括的なステートメントとしてIgnoreArgumentsを指示するだけです。 Moqでは、引数ごとにIt.IsAny()を指定する必要があるようです。ただし、これはref引数とout引数では機能しません。特定の結果を返すために内部サービス呼び出しをMoqする必要がある場合、次のメソッドをテストするにはどうすればよいですか。

public void MyMethod() {
    // DoStuff

    IList<SomeObject> errors = new List<SomeObject>();
    var result = _service.DoSomething(ref errors, ref param1, param2);

    // Do more stuff
}

試験方法:

public void TestOfMyMethod() {
    // Setup
    var moqService = new Mock<IMyService>();
    IList<String> errors;
    var model = new MyModel();

    // This returns null, presumably becuase "errors" 
    // here does not refer to the same object as "errors" in MyMethod
    moqService.Setup(t => t.DoSomething(ref errors, ref model, It.IsAny<SomeType>()).
        Returns(new OtherType()));  
}

更新:したがって、エラーを「ref」から「out」に変更すると機能します。したがって、実際の問題は、挿入できないrefパラメーターがあることのようです。

23
sydneyos

すでに理解しているように、問題はref引数にあります。

Moqは現在、ref引数の完全一致のみをサポートしています。つまり、呼び出しは、Setupで使用したものと同じインスタンスを渡した場合にのみ一致します。したがって、一般的な一致はないため、It.IsAny()は機能しません。

Moqを参照 quickstart

// ref arguments
var instance = new Bar();
// Only matches if the ref argument to the invocation is the same instance
mock.Setup(foo => foo.Submit(ref instance)).Returns(true);

そしてMoq ディスカッショングループ

参照マッチングとは、メソッドが同じインスタンスで呼び出された場合にのみセットアップがマッチングされることを意味します。 It.IsAnyはnullを返すので、おそらくあなたが探しているものではありません。

セットアップで実際の呼び出しと同じインスタンスを使用すると、セットアップが一致します。

15
nemesv

@nemesvが前述したように、It.IsAnyはnullを返すため、refパラメーターとして使用することはできません。呼び出しが機能するためには、実際のオブジェクトがそれに渡される必要があります。

この問題は、参照で渡したいオブジェクトの作成にアクセスできない場合に発生します。 DIDが実際のオブジェクトにアクセスできる場合は、それをテストで使用するだけで、モックを作成することを忘れることができます。

これは、抽出とオーバーライドの手法を使用した回避策であり、これを実行できます。名前が示すように、問題のあるコードを独自のメソッドに抽出します。次に、テスト対象のクラスから継承するテストクラスのメソッドをオーバーライドします。最後に、実際のオブジェクトを設定し、それを新しく作成したテストクラスに渡し、必要に応じてref呼び出しでテストします。

これは(考案された)コードの長いビットですが、最後に合格テストがあり、前後を示しています。

using System;
using System.Collections.Generic;
using Moq;
using MoqRefProblem;
using NUnit.Framework;

namespace MoqRefProblem
{
    //This class is the one we want to have passed by ref.
    public class FileContext
    {
        public int LinesProcessed { get; set; }
        public decimal AmountProcessed { get; set; }
    }

    public interface IRecordParser
    {
        //The ref parameter below is what's creating the testing problem.
        void ParseLine(decimal amount, ref FileContext context);
    }

    //This is problematic because we don't have a 
    //seam that allows us to set the FileContext.
    public class OriginalFileParser
    {
        private readonly IRecordParser _recordParser;

        public OriginalFileParser(IRecordParser recordParser)
        {
            _recordParser = recordParser;
        }

        public void ParseFile(IEnumerable<decimal> items)
        {
            //This is the problem
            var context = new FileContext();
            ParseItems(items, ref context);
        }

        private void ParseItems(IEnumerable<decimal> items, ref FileContext context)
        {
            foreach (var item in items)
            {
                _recordParser.ParseLine(item, ref context);
            }
        }
    }

    }

    //This class has had the creation of the FileContext extracted into a virtual 
    //method. 
    public class FileParser
    {
        private readonly IRecordParser _recordParser;

        public FileParser(IRecordParser recordParser)
        {
            _recordParser = recordParser;
        }

        public void ParseFile(IEnumerable<decimal> items)
        {
            //Instead of newing up a context, we'll get it from a virtual method 
            //that we'll override in a test class.
            var context = GetFileContext();
            ParseItems(items, ref context);
        }

        //This is our extensibility point
        protected virtual FileContext GetFileContext()
        {
            var context = new FileContext();
            return context;
        }

        private void ParseItems(IEnumerable<decimal> items, ref FileContext context)
        {
            foreach (var item in items)
            {
                _recordParser.ParseLine(item, ref context);
            }
        }
    }

    //Create a test class that inherits from the Class under Test    
    //We will set the FileContext object to the value we want to
    //use.  Then we override the GetContext call in the base class 
    //to return the fileContext object we just set up.
    public class MakeTestableParser : FileParser
    {
        public MakeTestableParser(IRecordParser recordParser)
            : base(recordParser)
        {
        }

        private FileContext _context;

        public void SetFileContext(FileContext context)
        {
            _context = context;
        }

        protected override FileContext GetFileContext()
        {
            if (_context == null)
            {
                throw new Exception("You must set the context before it can be used.");
            }

            return _context;
        }
    }

[TestFixture]
public class WorkingFileParserTest
{
    [Test]
    public void ThisWillWork()
    {
        //Arrange
        var recordParser = new Mock<IRecordParser>();

        //Note that we are an instance of the TestableParser and not the original one.
        var sut = new MakeTestableParser(recordParser.Object);
        var context = new FileContext();
        sut.SetFileContext(context);

        var items = new List<decimal>()
            {
                10.00m,
                11.50m,
                12.25m,
                14.00m
            };

        //Act
        sut.ParseFile(items);

        //Assert
        recordParser.Verify(x => x.ParseLine(It.IsAny<decimal>(), ref context), Times.Exactly(items.Count));
    }
}
2
Doug Ferguson