【问题标题】:How can I use unit testing when classes depend on one another or external data?当类相互依赖或依赖外部数据时,如何使用单元测试?
【发布时间】:2011-04-09 12:42:04
【问题描述】:

我想开始使用单元测试,但我很难理解如何在我当前的项目中使用它们。

我当前的项目是一个将文件收集到“目录”中的应用程序。然后,Catalog 可以从它包含的文件中提取信息,例如缩略图和其他属性。用户还可以使用其他自定义元数据标记文件,例如“作者”和“注释”。它可以很容易地与 Picasa 或 Adob​​e Lightroom 等相册应用程序进行比较。

我已将用于创建和操作Catalog 的代码分离到一个单独的 DLL 中,我现在想对其进行测试。但是,我的大多数类从来都不是要自己实例化的。相反,一切都是通过我的Catalog 课程发生的。例如,我无法单独测试我的File 类,因为File 只能通过Catalog 访问。

作为单元测试的替代方案,我认为编写一个测试程序来运行一系列操作(包括创建目录、重新打开已创建的目录以及操作目录的内容)会更有意义。目录。请参阅下面的代码。

//NOTE: The real version would have code to log the results and any exceptions thrown

//input data
string testCatalogALocation = "C:\TestCatalogA"
string testCatalogBLocation = "C:\TestCatalogB"
string testFileLocation = "C:\testfile.jpg"
string testFileName = System.IO.Path.GetFileName(testFileLocation);


//Test creating catalogs
Catalog catAtemp = Catalog(testCatalogALocation)
Catalog catBtemp = Catalog(testCatalogBLocation );


//test opening catalogs
Catalog catA = Catalog.OpenCatalog(testCatalogALocation);
Catalog catB = Catalog.OpenCatalog(testCatalogBLocation );


using(FileStream fs = new FileStream(testFileLocation )
{
    //test importing a file
    catA.ImportFile(testFileName,fs);
}

//test retrieving a file
File testFile = catA.GetFile(System.IO.Path.GetFileName(testFileLocation));

//test copying between catalogs
catB.CopyFileTo(testFile);


//Clean Up after test
System.IO.Directory.Delete(testCatalogALocation);
System.IO.Directory.Delete(testCatalogBLocation);

首先,我错过了什么吗?有没有办法对这样的程序进行单元测试?其次,是否有某种方法可以像上面的代码一样创建过程类型测试,但能够利用 Visual Studio 中内置的测试工具? VS2010 中的“通用测试”是否允许我这样做?


更新

感谢大家的所有回复。实际上,我的类实际上确实继承自一系列接口。 Here's a class diagram 给任何有兴趣的人。实际上我有更多的接口然后我有类。为了简单起见,我只是省略了示例中的接口。

感谢所有使用模拟的建议。我过去听过这个词,但直到现在才真正理解“模拟”是什么。我了解如何创建 IFile 接口的模拟,它表示目录中的单个文件。我还了解如何创建 IATAlog 接口的模拟版本来测试两个目录如何交互。

但我不明白如何测试我的具体 IATAlog 实现,因为它们与后端数据源密切相关。实际上,我的 Catalog 类的全部目的是读取、写入和操作它们的外部数据/资源。

【问题讨论】:

    标签: c# .net unit-testing visual-studio-2010 tdd


    【解决方案1】:

    您应该阅读SOLID 代码原则。特别是 SOLID 上的“D”代表Dependency Injection/Inversion Principle,这是您尝试测试的类不依赖于其他具体类和外部实现,而是依赖于接口和抽象的地方。您依靠 IoC(控制反转)容器(例如 UnityNinjectCastle Windsor)在运行时动态注入具体依赖项,但在单元测试期间您注入的是模拟/存根。

    例如考虑以下类:

    public class ComplexAlgorithm
    {
        protected DatabaseAccessor _data;
    
        public ComplexAlgorithm(DatabaseAccessor dataAccessor)
        {
            _data = dataAccessor;
        }
    
        public int RunAlgorithm()
        {
            // RunAlgorithm needs to call methods from DatabaseAccessor
        }
    }
    

    RunAlgorithm() 方法需要访问数据库(通过 DatabaseAccessor),因此难以测试。因此,我们改为将 DatabaseAccessor 更改为接口。

    public class ComplexAlgorithm
    {
        protected IDatabaseAccessor _data;
    
        public ComplexAlgorithm(IDatabaseAccessor dataAccessor)
        {
            _data = dataAccessor;
        }
    
        // rest of class (snip)
    }
    

    现在 ComplexAlgorithm 依赖于 IDatabaseAccessor 接口,当我们需要单独对 ComplexAlgorithm 进行单元测试时,可以轻松地模拟该接口。例如:

    public class MyFakeDataAccessor : IDatabaseAccessor
    {
        public IList<Thing> GetThings()
        {
            // Return a fake/pretend list of things for testing
            return new List<Thing>()
            {
                new Thing("Thing 1"),
                new Thing("Thing 2"),
                new Thing("Thing 3"),
                new Thing("Thing 4")
            };
        }
    
        // Other methods (snip)
    }
    
    [Test]
    public void Should_Return_8_With_Four_Things_In_Database()
    {
        // Arrange
        IDatabaseAccessor fakeData = new MyFakeDataAccessor();
        ComplexAlgorithm algorithm = new ComplexAlgorithm(fakeData);
        int expectedValue = 8;
    
        // Act
        int actualValue = algorithm.RunAlgorithm();
    
        // Assert
        Assert.AreEqual(expectedValue, actualValue);
    }
    

    我们本质上是在将这两个类彼此“分离”。解耦是编写更易维护和更健壮的代码的另一个重要软件工程原则。

    就依赖注入、SOLID 和解耦而言,这确实是冰山一角,但它是您有效地对代码进行单元测试所需要的。

    【讨论】:

    • 谢谢,这真的帮助我理解了嘲笑,这是我以前从未真正理解过的。但是,就我而言,我的程序的真正内容是数据访问目录类。从您的示例来看,如果您的项目是 DatabaseAccessor。你怎么能测试它?
    【解决方案2】:

    这是一个可以帮助您入门的简单算法。还有其他技术可以解耦代码,但这通常会让您走得很远,尤其是在您的代码不是太大且根深蒂固的情况下。

    1. 确定您依赖外部数据/资源的位置,并确定您是否有隔离每个依赖项的类。

    2. 如有必要,重构以实现必要的绝缘。这是安全执行中最具挑战性的部分,因此请首先关注风险最低的更改。

    3. 为隔离外部数据的类提取接口。

    4. 在构建类时,将外部依赖项作为接口传递,而不是让类自己实例化它们。

    5. 创建不依赖于外部资源的接口的测试实现。这也是您可以为您的测试添加“感应”代码的地方,以确保正在使用适当的调用。模拟框架在这里非常有用,但是为一个简单的项目手动创建存根类可能是一个很好的练习,因为它可以让您了解您的测试类在做什么。手动存根类通常设置公共属性以指示何时/如何调用方法,并具有公共属性以指示特定调用的行为方式。

    6. 编写调用类上的方法的测试,使用存根依赖项来检测类在不同情况下是否在做正确的事情。如果您已经编写了功能代码,一个简单的开始方法是绘制不同的路径并编写涵盖不同情况的测试,断言当前发生的行为。这些被称为特征测试,它们可以让您有信心开始重构您的代码,因为现在您知道您至少没有改变您已经建立的行为。

    祝你好运。编写好的单元测试需要改变视角,当您努力识别依赖关系并为测试创建必要的隔离时,这将自然而然地发展。起初,代码会感觉更难看,增加了以前不必要的间接层,但是当你学习各种隔离技术和重构(现在你可以更容易地做到这一点,通过测试来支持它),你可能会发现事情实际上变得更简洁、更容易理解。

    【讨论】:

    • "1) 确定您依赖外部数据/资源的位置,并确定您是否有隔离每个依赖项的类。 . . ... .看到我的问题是我的程序的目的是读取,写入和操作外部数据。所以是的,我有一个隔离这个功能的类(ICatalog/Catalog),但是这个接口/类也是我想测试的程序的真正内容。
    • @Eric,如果它是您程序的核心,那么它并没有真正完全隔离依赖关系。有太多的逻辑与隔离交织在一起。考虑您对外部数据的所有调用,看看您是否可以将它们提取到一个适配器对象中,该对象只是将调用转发到外部依赖项。一旦你有了这个,你现在就有了一个提供明确隔离的类,没有额外的责任。此时,您可以继续执行其他步骤,提取接口并使用它进行测试。
    • My Catalog 类,它实现了 ICatalog,实际上是一个抽象类,它通过引用额外的抽象方法来实现 ICatalog,这些抽象方法简单地从目录中读取和写入数据(WriteFile()、ReadFile()、 CreateDBRecord(), ReadDBRecord() 等。这些抽象方法然后在 Catalog 的具体实现中定义。所以你说的是不是让这些方法抽象地封装在一个接口中,比如 ICatalogDataAdapter。所以不同的实现目录实际上只是 IATAlogDataAdapters 的不同实现。对吧?
    • @Eric,我不确定我是否完全理解这些方法在做什么,但我认为你走在正确的轨道上。您可能需要为 Catalog 的不同具体实现创建不同的适配器,但这实际上取决于您正在执行的操作类型。我会列出你的代码中所有纯粹访问外部数据的地方(例如从流中读取或执行 SQL 查询),看看你是否可以识别出你可能想要为测试伪造特定数据的自然边界。
    【解决方案3】:

    这是一个依赖注入发挥重要作用的纯粹案例。

    正如 Shady 建议阅读有关嘲笑和存根的内容。为了实现这一点,您应该考虑使用一些依赖注入器,例如(.net 中的 Unity)。

    还可以阅读这里的依赖注入

    http://martinfowler.com/articles/injection.html

    【讨论】:

    • 即使不使用 DI,您仍然可以模拟/存根对象。一些模拟框架,如 TypeMock、Moles(来自 Microsoft 研究)和我猜 JustMock(来自 Telerik)几乎可以模拟/存根每个您可能会想到的单个对象.. 然而,正如您所说,从长远来看,改变设计是更好的投资
    【解决方案4】:

    我的大部分课程从来没有 打算自己实例化

    这就是 D - 设计 D - 进入 TDD 的地方。拥有紧密耦合的类是糟糕的设计。当您尝试对这样的类进行单元测试时,这种坏处就会立即显现出来——如果您开始进行单元测试,您将永远不会遇到这种情况。编写可测试的代码促使我们进行更好的设计。

    对不起;这不是您问题的答案,但我看到其他人已经提到了嘲笑和 DI,这些答案很好。但是你在这个问题上加上了 TDD 标签,这就是 TDD 对你问题的回答:不要把自己置于紧耦合类的境地。

    【讨论】:

    • 好点。有时,最好的答案是出乎你意料的。
    【解决方案5】:

    你现在拥有的是Legacy Code。即:未经测试就已实现的代码。对于您的初始测试,我肯定会通过 Catalog 类进行测试,直到您可以打破所有这些依赖关系。因此,您的第一组测试将是集成/验收测试。

    如果您不希望任何行为发生变化,那么就这样吧,但如果您确实进行了更改,我建议您 TDD 进行更改并使用更改构建单元测试。

    【讨论】:

      猜你喜欢
      • 2015-01-16
      • 2012-07-15
      • 2013-12-08
      • 2012-03-28
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-09-18
      相关资源
      最近更新 更多