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

时间:2016-08-19 14:32:59

标签: c# unit-testing architecture refactoring

想象一下这样的课程:

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以便对该逻辑进行单元测试,因为它是来自Microsoft的没有接口的外部类。

我可以将if语句推送到像这样的单独函数中:

public bool HasSingleQuote(string lineToCheck)
{
    return lineToCheck.Contains("'");
}

但这些不需要在课外访问。他们也不需要从其他地方打电话,所以他们不属于辅助类或类似的。因此,根据良好的设计原则,它们是私有的而非公开的,我应该通过他们的公共访问者来测试它们。在这种情况下,取决于不可测试的TextFieldParser。

我可以将TextFieldParser包装在我自己的类中并粘贴和接口,但感觉就像过度杀戮和不必要的代码复制。

我很欣赏这是一个不值得测试的简单例子,但我只是把它放在一起来说明问题。重构此代码以使我的逻辑可测试的最佳方法是什么?

2 个答案:

答案 0 :(得分:2)

我会说测试你拥有的东西。 TextFieldParser是一个实现细节。 MS会广泛测试它的发布功能。如果关注的是你正在进行条件检查的实现内部的逻辑,那么可以认为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);
        }
    }
}

答案 1 :(得分:2)

测试问题是基于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的该服务的默认实现,但您也可以编写一个只返回一组预定义数据的测试实现。