代码之家  ›  专栏  ›  技术社区  ›  Polaris

具有文件系统依赖项的TDD

  •  6
  • Polaris  · 技术社区  · 14 年前

    加载文件成功加载数据() . 我想将它重构为单元测试,以打破与文件系统的依赖关系。

    P、 我是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;
        }
    }
    

    还有我的测试班(努尼特)

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

    11 回复  |  直到 12 年前
        1
  •  8
  •   Steven    14 年前

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

    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 接口。该接口的实现可以在本地化数据的构造函数中提供。现在可以按如下方式编写单元测试:

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

    现在在您的测试环境中,您可以(而且可能应该)始终手动创建要测试的类。但是,您希望抽象工厂方法中的创建,以防止在被测类更改时必须更改许多测试。

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

    也请不要说你可能不想更新你的 在您自己的产品代码中,因为这个类可能被依赖于这个类型的其他类使用。通常,您会要求框架为您创建最顶级的类(例如实现完整用例的命令)并执行它。

    我希望这有帮助。

        2
  •  6
  •   Robert Martin    12 年前

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

    第一个测试是退化情况。给定一个类似“name_with_wrong_ending”的字符串,函数应该会失败。怎么会失败?是否应返回空值?还是应该抛出一个例外?你得考虑一下,在考试中做出决定。然后你就通过了考试。

    现在,像这样的字符串怎么样:“test_file.xml”但是在不存在这样的文件的情况下呢?在这种情况下,您希望函数做什么?是否应返回空值?它应该抛出异常吗?

    当然,测试这一点的最简单方法是在一个没有该文件的目录中实际运行代码。但是,如果您希望编写测试以使其不使用文件系统(明智的选择),那么您需要能够询问“此文件是否存在”,然后您的测试需要强制将答案设置为“false”。

    当然,现在您必须测试“isFilePresent”的正常实现是否正常工作。为此,您必须使用真正的文件系统。但是,通过创建一个名为file system的新类并将“isFilePresent”方法移动到该新类中,可以将文件系统测试排除在本地化数据测试之外。然后,本地化数据测试可以创建该新文件系统类的派生,并重写“isFilePresent”以返回false。

    存在,但不包含有效的xml?有什么用吗?还是客户有问题?你自己决定。但如果你决定检查它,你可以使用与以前相同的策略。生成一个名为isValidXML的函数,并让测试重写它以返回false。

    最后,我们需要编写实际返回XMLDataProvider的测试。因此,在所有其他函数之后,“loadData”应该调用的最后一个函数是createXmlDataProvider。您可以重写它以返回空的或伪的XmlDataProvider。

        3
  •  3
  •   Naresh Jain    12 年前

    当我看到以下代码时:

    public class LocalizationData
    {
        private static bool IsXML(string fileName)
        {
            return (fileName != null && fileName.ToLower().EndsWith("xml"));
        }
    
        public XmlDataProvider LoadFile(string fileName)
        {
            if (!IsXML(fileName)) return null*;
            return new XmlDataProvider{
                                         IsAsynchronous = false,
                                         Source = new Uri(fileName, UriKind.Absolute)
                                      };
        }
    }
    

    无论如何,我会问自己以下问题:

    • 有什么可能破坏这个代码?有没有复杂的逻辑或脆弱的代码,我应该安全防范自己?
    • 有什么复杂的东西需要理解,或者值得通过一个测试来强调,代码不能通信吗?

    LoadFile函数在获得有效的XML文件名时创建同步XmlDataProvide。

    一旦我们解决了这个问题,我们只需要知道XMLDataProvider处于同步模式是否有任何特殊原因。

    现在,有什么值得测试的吗?XMLDataProvider不是我们构建的类,我们希望它在提供有效Uri时能够正常工作。

    坦白说,我会 为此编写测试。在未来,如果我们看到更多的逻辑潜入,我们可能会再次讨论这个问题。

        4
  •  2
  •   AndiDog    14 年前

    我不知道如何在C#中实现这一点,但也许您可以在测试中测试文件“data/azeri.xml”的存在性 SetUp 方法。

        5
  •  2
  •   Martin R-L    14 年前

    它与您的测试(x)无关,但是考虑使用 Uri 而不是 String 作为API的参数类型。

    http://msdn.microsoft.com/en-us/library/system.uri(v=VS.100).aspx

    x: 我认为史蒂文对那个话题讲得很好。

        6
  •  1
  •   Drew Noakes    14 年前

    为什么使用XmlDataProvider?我不认为这是一个有价值的单元测试。相反,你为什么不测试一下你会用那个数据提供者做什么呢?

    例如,如果使用XML数据加载 Foo 对象,生成接口:

    public interface IFooLoader
    {
        IEnumerable<Foo> LoadFromFile(string fileName);
    }
    

    对于使用此类型的协作者,可以传入模拟版本。您可以手工编写mock,也可以使用Moq、Rhino、TypeMock或NMock等mock框架。模仿是很好的,但是如果你是TDD新手,那么在你学习模仿有用的东西的同时手工编写你的模仿是很好的。一旦你拥有了它,你就可以很好地理解模仿框架的好坏和丑陋。当你开始TDD的时候,他们可能会有点麻烦。你的里程可能不同。

        7
  •  0
  •   Rod    14 年前

    在这种情况下,您基本上处于较低的依赖级别。您正在测试文件是否存在,以及是否可以使用该文件作为源创建xmlprovider。

    XmlDataProvider . 然后你可以嘲笑它返回一个 XML数据提供程序

    class XmlDataProviderFactory
    {
        public virtual XmlDataProvider NewXmlDataProvider(string fileName)
        {
           return  new XmlDataProvider
                       {
                           IsAsynchronous = false,
                           Source = new Uri(fileName, UriKind.Absolute)
                       };
    }
    
    class XmlDataProviderFactoryMock : XmlDataProviderFactory
    {
        public override XmlDataProvider NewXmlDataProvider(string fileName)
        {
            return new XmlDataProvider();
        }
    }
    
    public class LocalizationData
    {
    ...
        public XmlDataProvider LoadFile(string fileName, XmlDataProviderFactory factory)
            {
                if (IsValidFileName(fileName))
                {
                    return factory.NewXmlDataProvider(fileName);
                }
                return null;
            }
    }
    
    [TestFixture]
    class LocalizationDataTest
    {
        [Test]
        public void LoadFile_DataLoaded_Succefully()
        {
            var data = new LocalizationData();
            string fileName = "d:/azeri.xml";
            XmlDataProvider result = data.LoadFile(fileName, new XmlDataProviderFactoryMock());
            Assert.IsNotNull(result);
            Assert.That(result.Document, Is.Not.Null);
        }
    
    }
    

    使用注入框架可以简化对 LoadFile 通过在类构造函数或其他地方注入工厂。

        8
  •  0
  •   Gutzofter    14 年前

    public interface DataProvider
    {
        bool IsValidProvider();
        void DisableAsynchronousOperation();
    }
    
    public class XmlDataProvider : DataProvider
    {
        private string fName;
        private bool asynchronousOperation = true;
    
        public XmlDataProvider(string fileName)
        {
            fName = fileName;
        }
    
        public bool IsValidProvider()
        {
            return fName.ToLower().EndsWith("xml");
        }
    
        public void DisableAsynchronousOperation()
        {
            asynchronousOperation = false;
        }
    }
    
    
    public class LocalizationData
    {
        private DataProvider dataProvider;
    
        public LocalizationData(DataProvider provider)
        {
            dataProvider = provider;
        }
    
        public DataProvider Load()
        {
            if (provider.IsValidProvider())
            {
                provider.DisableAsynchronousOperation();
                return provider;
            }
            return null;
        }
    }
    

    Last Possible Responsible Moment . 将尽可能多的内容下推到 DataProvider

    我没有对这段代码做一件事,那就是用单元测试和模拟来驱动它。这就是为什么您仍然要检查提供者的状态,看看它是否 .

    另一件事是,我试图删除依赖于让本地化数据知道提供者正在使用文件的依赖关系。如果是web服务或数据库呢?

        9
  •  0
  •   Parag    12 年前

    所以首先让我们了解我们需要测试什么。我们需要验证给定一个有效的文件名,LoadFile(fn)方法返回一个XmlDataProvider,否则返回null。

    创建 具有从文件名创建的URI的XmlDataProvider。我在C#上没有做过很多工作,但是我假设如果系统中不存在该文件,我们将得到一个异常。 真正的问题是,您的生产方法LoadFile()正在创建一些很难伪造的东西

    所以解决方案是-我们应该能够伪造loadFile方法的协作者(XmlDataProvider)。然而,如果一个方法创建了它的合作者,它就不能伪造它们,因此一个方法不应该创建它的合作者。

    1. 它们应该被注入到这个方法中
    2. 应该是从某个工厂买的

    有趣的部分来了。当您的代码在生产环境中运行时,工厂应该返回一个XmlDataProvider,当您的代码在测试环境中运行时,工厂应该返回一个假对象。

    希望这有帮助。

        10
  •  0
  •   J. B. Rainsberger    12 年前

    这次,不要试图打破对文件系统的依赖。此行为显然取决于文件系统,并且似乎处于与文件系统的集成点,因此请使用文件系统对其进行测试。

        11
  •  0
  •   Gishu    12 年前
    • 与其返回绑定特定技术的XmlDataProvider,不如隐藏此实现详细信息。看起来您需要一个存储库角色来

    您可以为这个角色提供一个实现,它在内部使用Xml。您需要编写集成测试来测试XmlLocalizationDataRepository是否可以读取实际的Xml数据存储。(慢)。