24

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

4

3 回答 3

15

正如您已经发现问题在于您的ref论点。

Moq 目前仅支持ref参数的精确匹配,这意味着调用仅在您传递您在Setup. 所以没有一般​​匹配,所以It.IsAny()不会起作用。

请参阅最小起订量快速入门

// 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);

起订量讨论组

Ref 匹配意味着只有在使用同一个实例调用方法时才匹配设置。It.IsAny 返回 null,因此可能不是您要查找的内容。

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

于 2012-06-06T04:52:58.877 回答
2

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

当您无权访问要通过 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));
    }
}
于 2013-05-09T23:43:54.410 回答
-1

回答于:设置最小起订量以忽略虚拟方法 我相信在模拟上设置“CallBase = true”会起作用。请参阅快速入门的“自定义模拟行为”部分

于 2013-05-09T12:18:54.697 回答