如何设计可测试的代码与不可测试的函数绑定
本文关键字:测试 函数 绑定 代码 | 更新日期: 2023-09-27 18:03:19
想象这样一个类:
public class FileParser : IFileParser
{
public string ParseFirstRowForDelimiters(string path)
{
using (TextFieldParser parser = new TextFieldParser(path))
{
string line = parser.ReadLine();
if(lineContains("'"))
{
return "'";
}
if(lineContains("'"")
{
return "'"";
}
return "";
}
}
}
对于依赖FileParser的类,我可以通过它的接口模拟它的函数,一切都很好。然而,类本身内部的逻辑依赖于TextFieldParser返回一行进行检查。
我不能"接口"的TextFieldParser与模拟为了单元测试逻辑,因为它是一个外部类从微软没有接口。
我可以把if语句放到单独的函数中,像这样:
public bool HasSingleQuote(string lineToCheck)
{
return lineToCheck.Contains("'");
}
但是这些不需要在类外可访问。也不需要从其他地方调用它们,因此它们不属于helper类或类似的类。因此,根据良好的设计原则,它们应该是私有的而不是公共的,我应该通过它们的公共访问器来测试它们。在这种情况下,它依赖于不可测试的TextFieldParser。
我可以在我自己的类中包装TextFieldParser,并在其上粘贴和接口,但感觉像是过度的和不必要的代码复制。
我知道这是一个不值得测试的小例子,但我只是把它放在一起来说明这个问题。重构代码以使逻辑可测试的最佳方法是什么?
我会说测试你所拥有的。TextFieldParser
是一个实现细节。微软会在发布前对它的功能进行广泛的测试。如果关注的是它的实现内部的逻辑,你正在做你的条件检查,那么它可以认为IFileParser
实现可能做太多的事情。我想起了SRP,只有一个改变的理由。
public interface IDelimiterLogic {
string Invoke(string line);
}
与类似
的实现public class DefaultDelimiterLogic : IDelimiterLogic {
public string Invoke(string line) {
if (line.Contains("'")) {
return "'";
}
if (line.Contains("'"")) {
return "'"";
}
return "";
}
}
FileParser的实现将被重构为…
public class FileParser : IFileParser {
IDelimiterLogic delimiterLogic;
public FileParser(IDelimiterLogic delimiterLogic) {
this.delimiterLogic = delimiterLogic;
}
public string ParseFirstRowForDelimiters(string path) {
using (TextFieldParser parser = new TextFieldParser(path)) {
string line = parser.ReadLine();
return delimiterLogic.Invoke(line);
}
}
}
所以现在如果你想测试你的分隔符逻辑,测试的系统将是IDelimiterLogic
的实现。
也归功于@JAllen对第三方依赖的抽象。
public interface ITextFieldParser : IDisposable {
bool EndOfData { get; }
string ReadLine();
}
public interface ITextFieldParserFactory {
ITextFieldParser Create(string path);
}
public class TextFieldParserFactory : ITextFieldParserFactory {
public ITextFieldParser Create(string path) {
return new TextFieldParserWrapper(path);
}
}
public class TextFieldParserWrapper : ITextFieldParser {
TextFieldParser parser;
internal TextFieldParserWrapper(string path) {
parser = new TextFieldParser(path);
}
public bool EndOfData { get{ return parser.EndOfData; } }
public string ReadLine() { return parser.ReadLine(); }
public void Dispose() { parser.Dispose(); }
}
新的重构IFileParser
实现
public class FileParser : IFileParser {
IDelimiterLogic delimiterLogic;
ITextFieldParserFactory parserFactory;
public FileParser(IDelimiterLogic delimiterLogic, ITextFieldParserFactory parserFactory) {
this.delimiterLogic = delimiterLogic;
this.parserFactory = parserFactory;
}
public string ParseFirstRowForDelimiters(string path) {
using (ITextFieldParser parser = parserFactory.Create(path)) {
string line = parser.ReadLine();
return delimiterLogic.Invoke(line);
}
}
}
测试问题是基于TextFieldParser是第三方依赖的事实,正确吗?你可以使用的一种策略是将第三方依赖包装在一个服务接口中,然后传递给FileParser。
public interface ITextFieldParserService
{
string ReadLine();
}
public class DefaultTextFieldParserService : ITextFieldParserService
{
private TextFieldParser parser;
public ITextFieldParserService Setup(string path)
{
parser = new TextFieldParser(path);
}
//you'd want some teardown method to dispose of TextFieldParser, or make
//the service IDisposable probably
}
public class FileParser : IFileParser
{
public FileParser(ITextFieldParserService textParserService)
{
}
...
public string ParseFirstRowForDelimiters(string path)
{
var parser = textParserService.Setup(path)
string line = parser.ReadLine();
if(lineContains("'"))
{
return "'";
}
if(lineContains("'"")
{
return "'"";
}
return "";
}
你可以有该服务的默认实现,它实际上使用第三方TextFieldParser,但你也可以编写一个测试实现,它只返回一组预定义的数据。