Подтвердить что ты не робот

Как заставить 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()));  
}

UPDATE: Таким образом, изменяются ошибки с "ref" на "out". Поэтому кажется, что в реальной проблеме есть параметр ref, который вы не можете ввести.

4b9b3361

Ответ 1

Как вы уже выяснили, проблема связана с вашим аргументом 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, поэтому, вероятно, не то, что вы ищете.

Использовать тот же экземпляр в настройке, что и в действительном вызове, и настройка будет соответствовать.

Ответ 2

Как упоминалось ранее @nemesv, It.IsAny возвращает null, поэтому вы не можете использовать его как параметр ref. Для того, чтобы вызов работал, к нему должен быть передан фактический объект.

Проблема возникает, когда у вас нет доступа к созданию объекта, который вы хотите передать по ссылке. Если у вас есть доступ к реальному объекту, вы можете просто использовать его в своем тесте и забыть о том, чтобы вообще попытаться его высмеять.

Ниже приведено обходное решение, использующее метод Extract and Override, который позволит вам сделать именно это. Как следует из названия, вы извлекаете проблемный бит кода в свой собственный метод. Затем вы переопределяете метод в тестовом классе, который наследуется от тестируемого класса. Наконец, вы настраиваете свой реальный объект, передаете его в свой новый класс тестирования и проверяете ваши звонки по звонкам, как вам нравится.

Здесь длинный бит (надуманный) код, но он показывает до и после, с проходящим тестом в конце.

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