C# 对于单元测试,消除对StreamReader/FileStream的依赖的好方法是什么?

C# 对于单元测试,消除对StreamReader/FileStream的依赖的好方法是什么?,c#,.net,unit-testing,nunit,C#,.net,Unit Testing,Nunit,以下是场景: 我有一个方法,通过.NET中的FileStream和StreamReader读取文件。我想对这个方法进行单元测试,并以某种方式消除对StreamReader对象的依赖 理想情况下,我希望能够提供自己的测试数据字符串,而不是使用真实的文件。目前,该方法始终使用StreamReader.ReadLine方法。为了使这个测试成为可能,我现在修改设计的方法是什么 依赖于流和文本阅读器。然后您的单元测试可以使用MemoryStream和StringReader。(如有必要,也可以从测试程序集

以下是场景:

我有一个方法,通过.NET中的FileStream和StreamReader读取文件。我想对这个方法进行单元测试,并以某种方式消除对StreamReader对象的依赖


理想情况下,我希望能够提供自己的测试数据字符串,而不是使用真实的文件。目前,该方法始终使用StreamReader.ReadLine方法。为了使这个测试成为可能,我现在修改设计的方法是什么

依赖于
文本阅读器
。然后您的单元测试可以使用
MemoryStream
StringReader
。(如有必要,也可以从测试程序集中加载资源。)


请注意,最初是如何由
TextReader
声明的,而不是
StreamReader

最简单的解决方案是让方法接受流作为参数,而不是打开自己的文件流。您的实际代码可以像往常一样在文件流中传递,而您的测试方法可以使用不同的文件流作为测试数据,也可以使用一个装满了您想要测试的内容的内存流(这不需要文件)。

在我的脑海中,我想说这是一个很好的机会来研究该方法的优点


<>你可能想考虑重新设计你的方法,以便它需要一个返回文件内容的委托。一个委托(生产委托)可能使用System.IO中的类,而第二个委托(用于单元测试)直接以字符串的形式返回内容。

我认为其想法是将依赖注入TextReader并模拟它进行单元测试。我认为您只能嘲笑TextReader,因为它是一个抽象类

public class FileParser
{
    private readonly TextReader _textReader;

    public FileParser(TextReader reader)
    {
        _textReader = reader;
    }

    public List<TradeInfo> ProcessFile()
    {
        var rows = _textReader.ReadLine().Split(new[] { ',' }).Take(4);
        return FeedMapper(rows.ToList());
    }

    private List<TradeInfo> FeedMapper(List<String> rows)
    {
        var row = rows.Take(4).ToList();
        var trades = new List<TradeInfo>();
        trades.Add(new TradeInfo { TradeId = row[0], FutureValue = Convert.ToInt32(row[1]), NotionalValue = Convert.ToInt32(row[3]), PresentValue = Convert.ToInt32(row[2]) });
        return trades;
    } 
}
公共类文件解析器
{
私有只读文本阅读器_TextReader;
公共文件解析器(文本阅读器)
{
_文本阅读器=阅读器;
}
公共列表进程文件()
{
var rows=_textReader.ReadLine().Split(新[]{',}).Take(4);
返回FeedMapper(rows.ToList());
}
专用列表FeedMapper(列表行)
{
var row=rows.Take(4.ToList();
var交易=新列表();
trades.Add(newtradeinfo{TradeId=row[0],FutureValue=Convert.ToInt32(row[1]),initialvalue=Convert.ToInt32(row[3]),PresentValue=Convert.ToInt32(row[2]);
回报交易;
} 
}
然后使用Rhino Mock进行模拟

public class UnitTest1
{
    [Test]
    public void Test_Extract_First_Row_Mocked()
    {            
        //Arrange
        List<TradeInfo> listExpected = new List<TradeInfo>();
        var tradeInfo = new TradeInfo() { TradeId = "0453", FutureValue = 2000000, PresentValue = 3000000, NotionalValue = 400000 };
        listExpected.Add(tradeInfo);
        var textReader = MockRepository.GenerateMock<TextReader>();
        textReader.Expect(tr => tr.ReadLine()).Return("0453, 2000000, 3000000, 400000");
        var fileParser = new FileParser(textReader);
        var list = fileParser.ProcessFile();           
        listExpected.ShouldAllBeEquivalentTo(list);         

    }
}
公共类UnitTest1
{
[测试]
公共无效测试\u摘录\u第一行\u模拟()
{            
//安排
List listExpected=新列表();
var tradeInfo=new tradeInfo(){TradeId=“0453”,未来价值=2000000,当前价值=3000000,概念价值=400000};
添加(贸易信息);
var textReader=MockRepository.GenerateMock();
Expect(tr=>tr.ReadLine()).Return(“0453200000000030000400000”);
var fileParser=新的fileParser(textReader);
var list=fileParser.ProcessFile();
listExpected.应与列表中的所有内容等效;
}
}
但问题在于,从客户机代码传递这样一个对象是否是一种好的做法,而我认为应该在负责处理的类中使用它来管理它。我同意在实际代码中使用sep委托,在单元测试中使用sep委托的想法,但这在生产中还是有点多余的代码。我可能对依赖注入和模拟文件IO打开/读取的想法有点迷茫,这实际上不是单元测试的候选对象,但文件处理逻辑是可以通过传递文件的字符串内容来测试的(AAA23^YKL890^300000^TTRFGYUBARC)


有什么想法吗!谢谢

,但这不只是暂时解决问题吗?任何使用“reader类”的类都需要创建
StreamReader
TextReader
,否则我会错过什么吗?@royalTS:他们可以使用任何
TextReader
Stream
实现。在测试中,它可能是一个
StringReader
;在真正的代码中,它可能是一个
StreamReader
包装一个文件,或者是一个网络流等等(问题是OP是否真的需要
文本阅读器
,或者只是一个
文本阅读器
),但关键是,通过向上移动抽象层,您可以获得更大的灵活性。