如何让Moq忽略ref或out参数。

24
在RhinoMocks中,您可以告诉您的模拟对象忽略所有参数。而在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参数。


你能发布 DoSomething 的签名吗?因为在你的示例中,它有 3 个参数 out errors, param1, param2,但是在你的测试中只用了两个参数 out errors, It.IsAny<SomeType>()。也许你正在模拟错误的重载,因为你的代码应该仍然可以工作,请参见 moq 帮助方法部分 - nemesv
那只是一个例子 - 但我已经更新了测试以并行执行示例代码。 - sydneyos
3个回答

15

正如你已经发现的那样,问题出在你的ref参数上。

目前Moq仅支持对ref参数进行精确匹配,这意味着只有当你传递与你在Setup中使用的相同实例时,调用才会匹配。因此没有通用的匹配,所以It.IsAny()不起作用。

请参阅Moq 快速入门

// 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的讨论组:

Ref匹配意味着只有当方法与该实例调用匹配时,设置才会被匹配。IsAny返回null,所以可能并不是你想要的。

在设置中使用与实际调用相同的实例,设置将匹配。


2
那么,在存在嵌套调用的情况下,如果ref/out变量是在调用我试图模拟的方法内设置的,我就会陷入困境。有没有任何不具备这种限制的模拟框架可以建议? - sydneyos
我认为RhinoMocks的IgnoreArguments()选项应该可以解决这个问题 - 我会尝试一下。 - sydneyos
更新,针对这些情况,我们不得不转向RhinoMocks。实现看起来像这样:IList<Errors> errors; _repository.Stub(t => t.MethodName(out errors).OutRef(new List<Errors>()).IgnoreArguments(); - sydneyos

2
正如 @nemesv 先前提到的,It.IsAny 返回 null,因此您无法将其用作 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));
    }
}

-1

回答于:设置 Moq 忽略虚方法 我相信在模拟对象上设置 "CallBase = true" 将会起作用。请参阅快速入门中的 "自定义模拟行为" 部分。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接