如何使Moq忽略ref或out的参数

时间:2012-06-04 23:38:07

标签: c# moq

在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参数。

3 个答案:

答案 0 :(得分:15)

正如您已经发现问题在于您的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 discussion group

  

参考匹配意味着仅在方法匹配时才匹配设置   用同一个实例调用。 It.IsAny很可能返回null   不是你想要的。

     

在设置中使用与实际呼叫中相同的实例,并且   设置将匹配。

答案 1 :(得分:2)

正如之前提到的@nemesv,It.IsAny返回null,因此您不能将它用作ref参数。为了使调用工作,需要将实际对象传递给它。

如果您无权访问要通过ref传递的对象的创建,则会出现此问题。如果你有权访问真实对象,你可以在测试中使用它,而忘记尝试模拟它。

这是一个使用Extract and Override技术的解决方法,可以让你做到这一点。顾名思义,您将有问题的代码段提取到自己的方法中。然后,重写继承自测试类的测试类中的方法。最后,您设置了您的真实对象,将其传递到新创建的测试类中,并根据需要测试您的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 :(得分:-1)

回答于:Setting up Moq to ignore a virtual method 我相信在模拟上设置“CallBase = true”会起作用。请参阅快速入门

的“自定义模拟行为”部分