【问题标题】:How to design testable code bound up with non-testable functions如何设计与不可测试功能绑定的可测试代码
【发布时间】:2016-08-20 01:03:22
【问题描述】:

想象一个这样的类:

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("'");
}

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

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

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

【问题讨论】:

  • 我会说测试你拥有的东西。 TextFieldParser 是一个实现细节。 MS 会广泛测试它的发布功能。如果担心的是您正在执行条件检查的其实现内部的逻辑,那么可以认为IFileParser 实现可能做了太多事情。我想起了 SRP,只有一个理由改变。
  • 顺便说一句,《有效地使用遗留代码》是一本很棒的书。 informit.com/store/… 第 14 章和第 15 章肯定适用于此。

标签: c# unit-testing architecture refactoring


【解决方案1】:

我会说测试你拥有的东西。 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 以及抽象第 3 方依赖项。

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);
        }
    }
}

【讨论】:

  • 谢谢。这与我提出的将事物推入另一个类的解决方案非常接近(尽管您的实现更好)。但这不会导致小类的激增吗?
  • 拥有许多能做好一件事的小类比在难以维护和测试的情况下将所有东西耦合在一起要好。它使事情保持稳定
【解决方案2】:

测试问题基于 TextFieldParser 是第 3 方依赖项这一事实,对吗?您可以使用的一种策略是将第 3 方依赖项包装在服务接口中,然后将其传递给 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 "";         
   }

您可以有一个实际使用第 3 方 TextFieldParser 的服务的默认实现,但您也可以编写一个仅返回一组预定义数据的测试实现。

【讨论】:

    猜你喜欢
    • 2013-06-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-12-10
    • 2020-03-13
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多