如何设计可测试的代码与不可测试的函数绑定

本文关键字:测试 函数 绑定 代码 | 更新日期: 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,但你也可以编写一个测试实现,它只返回一组预定义的数据。