正如@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));
}
}