Как заставить 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, который вы не можете ввести.
Ответы
Ответ 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));
}
}
Ответ 3
Отвечено на: Настройка Moq для игнорирования виртуального метода
Я считаю, что установка "CallBase = true" на макет будет работать. См. Раздел "Настройка макета" в разделе "Быстрый старт"