How to make Moq ignore arguments that are ref or out

时光毁灭记忆、已成空白 提交于 2019-12-18 18:38:19

问题


In RhinoMocks, you can just tell your mocks to IgnoreArguments as a blanket statement. In Moq, it seems, you have to specify It.IsAny() for each argument. However, this doesn't work for ref and out arguments. How can I test the following method where I need to Moq the internal service call to return a specific result:

public void MyMethod() {
    // DoStuff

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

    // Do more stuff
}

Test method:

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()));  
}

UPDATE: So, changing errors from "ref" to "out" works. So it seems like the real issue is having a ref parameter that you can't inject.


回答1:


As you already figured out the problem is with your ref argument.

Moq currently only support exact matching for ref arguments, which means the call only matches if you pass the same instance what you've used in the Setup. So there is no general matching so It.IsAny() won't work.

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

And Moq discussion group:

Ref matching means that the setup is matched only if the method is called with that same instance. It.IsAny returns null, so probably not what you're looking for.

Use the same instance in the setup as the one in the actual call, and the setup will match.




回答2:


As @nemesv mentioned previously, It.IsAny returns null so you can't use it as a ref parameter. In order for the call to work, an actual object needs to be passed to it.

The problem occurs when you don't have access to the creation of the object you are wanting to pass by ref. If you DID have access to the real object, you could simply use that in your test, and forget about trying to mock it at all.

Here is a workaround using the Extract and Override technique that will allow you to do just that. As the name implies, you extract the problematic bit of code into its own method. Then, you override the method in a test class that inherits from the class under test. Finally, you set up your real object, pass it into your newly create test class, and test your by ref calls as you please.

Here's long bit of (contrived) code, but it shows the before and after, with a passing test at the end.

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



回答3:


Answered at: Setting up Moq to ignore a virtual method I believe setting "CallBase = true" on the mock will work. See the "Customizing Mock Behavior" section of the Quick Start



来源:https://stackoverflow.com/questions/10889810/how-to-make-moq-ignore-arguments-that-are-ref-or-out

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!