当类依赖于彼此或外部数据时,如何使用单元测试?

Eri*_*tas 5 .net c# tdd unit-testing visual-studio-2010

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

我当前的项目是一个将文件收集到"目录"中的应用程序.Catalog然后,A 可以从其包含的文件中提取信息,例如缩略图和其他属性.用户还可以使用其他自定义元数据标记文件,例如"作者"和"注释".它可以很容易地与Picasa或Adobe 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);
Run Code Online (Sandbox Code Playgroud)

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


更新

感谢大家的所有回复.实际上我的类确实从一系列接口继承.这是任何感兴趣的人的类图.实际上我有更多的接口然后我有类.为了简单起见,我只是从我的例子中省略了接口.

感谢所有使用模拟的建议.我过去听过这个词,但直到现在才真正理解"模拟"是什么.我理解如何创建我的IFile接口的模拟,它代表目录中的单个文件.我也理解如何创建ICatalog界面的模拟版本来测试两个目录的交互方式.

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

Sun*_*oot 9

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

例如,考虑以下课程:

public class ComplexAlgorithm
{
    protected DatabaseAccessor _data;

    public ComplexAlgorithm(DatabaseAccessor dataAccessor)
    {
        _data = dataAccessor;
    }

    public int RunAlgorithm()
    {
        // RunAlgorithm needs to call methods from DatabaseAccessor
    }
}
Run Code Online (Sandbox Code Playgroud)

RunAlgorithm()方法需要命中数据库(通过DatabaseAccessor),这使得测试变得困难.因此我们将DatabaseAccessor更改为接口.

public class ComplexAlgorithm
{
    protected IDatabaseAccessor _data;

    public ComplexAlgorithm(IDatabaseAccessor dataAccessor)
    {
        _data = dataAccessor;
    }

    // rest of class (snip)
}
Run Code Online (Sandbox Code Playgroud)

现在,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);
}
Run Code Online (Sandbox Code Playgroud)

我们基本上将这两个类"脱钩".解耦是编写更易维护和健壮的代码的另一个重要的软件工程原理.

就依赖注入,SOLID和解耦而言,这实际上是冰山一角的一小部分,但这是为了有效地对代码进行单元测试所需要的.


Dan*_*ant 2

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

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

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

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

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

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

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

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