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;
    }
}
和我的考试班(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);
    }
}
任何想法如何重构它来打破文件系统依赖
你在这里缺少的是控制反转.例如,您可以在代码中引入依赖注入原则:
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;
    }
}
在上面的代码中,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";
}
该FakeXmlDataProviderFactory如下所示:
class FakeXmlDataProviderFactory : IXmlDataProviderFactory
{
    public XmlDataProvider ProviderToReturn { get; set; }
    public XmlDataProvider Create(string fileName)
    {
        return this.ProviderToReturn;
    }
}
现在,在您的测试环境中,您可以(并且可能应该)始终手动创建受测试的类.但是,您希望在工厂方法中抽象出创建,以防止在测试类发生更改时更改许多测试.
但是,在您的生产环境中,当您手动创建类时,它很快就会变得非常麻烦.特别是当它包含许多依赖项时.这就是IoC/DI框架闪耀的地方.他们可以帮助你.例如,当您想LocalizationData在生产代码中使用它时,您可能会编写如下代码:
var localizer = ServiceLocator.Current.GetInstance<LocalizationData>();
var data = data.LoadFile(fileName);
请注意,我在这里使用Common Service Locator作为示例.
该框架将负责为您创建该实例.但是,使用这样的依赖注入框架,您必须让框架知道您的应用程序需要哪些"服务".例如,当我使用Simple Service Locator库作为示例(无耻插件)时,您的配置可能如下所示:
var container = new SimpleServiceLocator();
container.RegisterSingle<IXmlDataProviderFactory>(
    new ProductionXmlDataProviderFactory());
ServiceLocator.SetLocatorProvider(() => container);
此代码通常位于应用程序的启动路径中.当然,拼图中唯一缺失的部分是实际ProductionXmlDataProviderFactory课程.就这个:
class ProductionXmlDataProviderFactory : IXmlDataProviderFactory
{
    public XmlDataProvider Create(string fileName)
    {
        return new XmlDataProvider
        {
            Source = new Uri(fileName, UriKind.Absolute)
        };
    }
}
另请注意,您可能不希望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文件的测试.
在我的一个(Python)项目中,我假设所有单元测试都在包含文件夹“data”(输入文件)和“output”(输出文件)的特殊目录中运行。我正在使用一个测试脚本,它首先检查这些文件夹是否存在(即当前工作目录是否正确),然后运行测试。然后,我的单元测试可以使用相对文件名,例如“data/test-input.txt”。
我不知道如何在 C# 中执行此操作,但也许您可以在测试方法中测试文件“data/azeri.xml”是否存在SetUp。