具有文件系统依赖性的TDD

Pol*_*ris 6 c# tdd nunit rhino-mocks

我有一个集成测试LoadFile_DataLoaded_Successfully().我想将它重构为单元测试以破坏与filesytem的依赖关系.

PS我是TDD的新手:

这是我的生产类:

public class LocalizationData
{
    private bool IsValidFileName(string fileName)
    {
        if (fileName.ToLower().EndsWith("xml"))
        {
            return true;
        }
        return false;
    }

    public XmlDataProvider LoadFile(string fileName)
    {
        if (IsValidFileName(fileName))
        {
            XmlDataProvider provider = 
                            new XmlDataProvider
                                 {
                                      IsAsynchronous = false,
                                      Source = new Uri(fileName, UriKind.Absolute)
                                 };

            return provider;
        }
        return null;
    }
}
Run Code Online (Sandbox Code Playgroud)

和我的考试班(Nunit)

[TestFixture]
class LocalizationDataTest
{
    [Test]
    public void LoadFile_DataLoaded_Successfully()
    {
        var data = new LocalizationData();
        string fileName = "d:/azeri.xml";
        XmlDataProvider result = data.LoadFile(fileName);
        Assert.IsNotNull(result);
        Assert.That(result.Document, Is.Not.Null);
    }
}
Run Code Online (Sandbox Code Playgroud)

任何想法如何重构它来打破文件系统依赖

Ste*_*ven 8

你在这里缺少的是控制反转.例如,您可以在代码中引入依赖注入原则:

public interface IXmlDataProviderFactory
{
    XmlDataProvider Create(string fileName);
}
public class LocalizationData
{
    private IXmlDataProviderFactory factory;
    public LocalizationData(IXmlDataProviderFactory factory)
    {
        this.factory = factory;
    }

    private bool IsValidFileName(string fileName)
    {
        return fileName.ToLower().EndsWith("xml");
    }

    public XmlDataProvider LoadFile(string fileName)
    {
        if (IsValidFileName(fileName))
        {
            XmlDataProvider provider = this.factory.Create(fileName);
            provider.IsAsynchronous = false;
            return provider;
        }
        return null;
    }
}
Run Code Online (Sandbox Code Playgroud)

在上面的代码中,XmlDataProvider使用IXmlDataProviderFactory接口抽象出创建.可以在LocalizationData的构造函数中提供该接口的实现.您现在可以按如下方式编写单元测试:

[Test]
public void LoadFile_DataLoaded_Succefully()
{
    // Arrange
    var expectedProvider = new XmlDataProvider();
    string validFileName = CreateValidFileName();
    var data = CreateNewLocalizationData(expectedProvider);

    // Act
    var actualProvider = data.LoadFile(validFileName);

    // Assert
    Assert.AreEqual(expectedProvider, actualProvider);
}

private static LocalizationData CreateNewLocalizationData(
    XmlDataProvider expectedProvider)
{
    return new LocalizationData(FakeXmlDataProviderFactory()
    {
        ProviderToReturn = expectedProvider
    });
}

private static string CreateValidFileName()
{
    return "d:/azeri.xml";
}
Run Code Online (Sandbox Code Playgroud)

FakeXmlDataProviderFactory如下所示:

class FakeXmlDataProviderFactory : IXmlDataProviderFactory
{
    public XmlDataProvider ProviderToReturn { get; set; }

    public XmlDataProvider Create(string fileName)
    {
        return this.ProviderToReturn;
    }
}
Run Code Online (Sandbox Code Playgroud)

现在,在您的测试环境中,您可以(并且可能应该)始终手动创建受测试的类.但是,您希望在工厂方法中抽象出创建,以防止在测试类发生更改时更改许多测试.

但是,在您的生产环境中,当您手动创建类时,它很快就会变得非常麻烦.特别是当它包含许多依赖项时.这就是IoC/DI框架闪耀的地方.他们可以帮助你.例如,当您想LocalizationData在生产代码中使用它时,您可能会编写如下代码:

var localizer = ServiceLocator.Current.GetInstance<LocalizationData>();

var data = data.LoadFile(fileName);
Run Code Online (Sandbox Code Playgroud)

请注意,我在这里使用Common Service Locator作为示例.

该框架将负责为您创建该实例.但是,使用这样的依赖注入框架,您必须让框架知道您的应用程序需要哪些"服务".例如,当我使用Simple Service Locator库作为示例(无耻插件)时,您的配置可能如下所示:

var container = new SimpleServiceLocator();

container.RegisterSingle<IXmlDataProviderFactory>(
    new ProductionXmlDataProviderFactory());

ServiceLocator.SetLocatorProvider(() => container);
Run Code Online (Sandbox Code Playgroud)

此代码通常位于应用程序的启动路径中.当然,拼图中唯一缺失的部分是实际ProductionXmlDataProviderFactory课程.就这个:

class ProductionXmlDataProviderFactory : IXmlDataProviderFactory
{
    public XmlDataProvider Create(string fileName)
    {
        return new XmlDataProvider
        {
            Source = new Uri(fileName, UriKind.Absolute)
        };
    }
}
Run Code Online (Sandbox Code Playgroud)

另请注意,您可能不希望LocalizationData自己创建生产代码,因为此类可能会被其他依赖此类的类使用.您通常要做的是让框架为您创建最顶级的类(例如,实现完整用例的命令)并执行它.

我希望这有帮助.


小智 6

这里的问题是你没有做TDD.您首先编写了生产代码,现在要对其进行测试.

删除所有代码并重新开始.首先编写测试,然后编写通过该测试的代码.然后写下一个测试等

你的目标是什么?给定一个以"xml"结尾的字符串(为什么不是".xml"?),您需要一个基于名称为该字符串的文件的XML数据提供程序.那是你的目标吗?

第一次测试将是退化的情况.给定一个像"name_with_wrong_ending"这样的字符串,你的函数应该会失败.怎么会失败?它应该返回null吗?或者它应该抛出异常?你可以考虑这个并决定你的测试.然后你进行测试通过.

现在,这样的字符串怎么样:"test_file.xml"但是在没有这样的文件的情况下?在这种情况下,您希望函数做什么?它应该返回null吗?它应该抛出异常吗?

当然,测试这个的最简单方法是在没有该文件的目录中实际运行代码.但是,如果您更愿意编写测试以使其不使用文件系统(明智的选择),那么您需要能够提出问题"这个文件是否存在",然后您的测试需要强制回答是"虚假的".

您可以通过在名为"isFilePresent"或"doesFileExist"的类中创建新方法来实现此目的.您的测试可以覆盖该函数以返回'false'.现在,您可以在文件不存在时测试"LoadFile"功能是否正常工作.

当然,现在你必须测试"isFilePresent"的正常实现是否正常工作.为此你必须使用真正的文件系统.但是,您可以通过创建名为FileSystem的新类并将"isFilePresent"方法移动到该新类中来保持文件系统测试不受LocalizationData测试的影响.然后,您的LocalizationData测试可以创建该新FileSystem类的派生,并覆盖'isFilePresent'以返回false.

您仍然需要测试FileSystem的常规实现,但这是在一组不同的测试中,只能运行一次.

好的,接下来的测试是什么?什么是你的"的loadFile"功能做当文件确实存在,但不包含有效的XML?它应该做什么吗?或者这对客户来说是个问题?你决定.但是如果您决定检查它,您可以使用与以前相同的策略.创建一个名为isValidXML的函数,并让测试覆盖它以返回false.

最后,我们需要编写实际返回XMLDataProvider的测试.所以'loadData'应该在所有其他函数之后调用的最终函数是createXmlDataProvider.您可以覆盖它以返回空的或虚拟的XmlDataProvider.

请注意,在您的测试中,您从未进入真正的文件系统,并且确实基于文件创建了XMLDataProvider.但你已经做的是检查每个if语句在loadData功能.您已经测试了loadData函数.

现在你应该再写一次测试了.使用真实文件系统和真实有效XML文件的测试.


And*_*Dog 2

在我的一个(Python)项目中,我假设所有单元测试都在包含文件夹“data”(输入文件)和“output”(输出文件)的特殊目录中运行。我正在使用一个测试脚本,它首先检查这些文件夹是否存在(即当前工作目录是否正确),然后运行测试。然后,我的单元测试可以使用相对文件名,例如“data/test-input.txt”。

我不知道如何在 C# 中执行此操作,但也许您可以在测试方法中测试文件“data/azeri.xml”是否存在SetUp

  • @Billy ONeal:任何测试单元(或模块)的东西在我看来都是单元测试,只要输入参数保持不变。如果输入参数太大或者模块进行文件操作(例如管理临时文件的模块),则单元测试需要读取或写入文件。 (2认同)