编写可测试的“从数据库导入数据”类

时间:2009-06-04 19:40:53

标签: c# database unit-testing class-design integration-testing

我的任务是从第三方供应商的SQLite数据表中提取所有行,从这些记录创建业务对象,并将新业务对象发送到另一个类。

的伪代码:

var databasePath = "%user profile%\application data\some3rdPartyVendor\vendor.sqlite"
var connection = OpenSqliteConnection(databasePath);
var allGizmoRecords = connection.Query(...);
var businessObjects = TransformIntoBizObjs(allGizmoRecords);
someOtherClass.HandleNewBizObjs(businessObjects);

我已经完成了所有工作。

我的问题是:如何编写此课程,以便它可以进行单元测试?

我应该:

  • 使用存储库模式模拟数据访问
  • 实际上在单元测试中提供了一个伪SQLite数据库

还是有更好的想法?我正在使用C#,但这个问题似乎与语言无关。

4 个答案:

答案 0 :(得分:2)

可以非常轻松地注入一个只测试的Sqlite数据库,重构代码如下所示。但你如何断言结果?业务对象将传递给someOtherClass。如果您注入ISomeOtherClass,那么该类的操作也需要可见。这似乎有点痛苦。

public class KillerApp
{
    private String databasePath;
    private ISomeOtherClass someOtherClass;

    public KillerApp(String databasePath, ISomeOtherClass someOtherClass)
    {
        this.databasePath = databasePath;
        this.someOtherClass = someOtherClass;
    }

    public void DoThatThing()
    {
        var connection = OpenSqliteConnection(databasePath);
        var allGizmoRecords = connection.Query(...);
        var businessObjects = TransformIntoBizObjs(allGizmoRecords);
        someOtherClass.HandleNewBizObjs(businessObjects);
    }
}

[TestClass]
public class When_Doing_That_Thing
{
    private const String DatabasePath = /* test path */;
    private ISomeOtherClass someOtherClass = new SomeOtherClass();
    private KillerApp app;

    [TestInitialize]
    public void TestInitialize()
    {
        app = new KillerApp(DatabasePath, someOtherClass);
    }

    [TestMethod]
    public void Should_convert_all_gizmo_records_to_busn_objects()
    {
        app.DoThatThing();
        Assert.AreEqual(someOtherClass.Results, /* however you're confirming */);
    }
}

使用IRepository会删除此类中的一些代码,允许您模拟IRepository实现,或伪造一个仅用于测试。

public class KillerApp
{
    private IRepository<BusinessObject> repository;
    private ISomeOtherClass someOtherClass;

    public KillerApp(IRepository<BusinessObject> repository, ISomeOtherClass someOtherClass)
    {
        this.repository = repository;
        this.someOtherClass = someOtherClass;
    }

    public void DoThatThing()
    {
        BusinessObject[] entities = repository.FindAll();
        someOtherClass.HandleNewBizObjs(entities);
    }
}

[TestClass]
public class When_Doing_That_Thing
{
    private const String DatabasePath = /* test path */;
    private IRepository<BusinessObject> repository;
    private ISomeOtherClass someOtherClass = new SomeOtherClass();
    private KillerApp app;

    [TestInitialize]
    public void TestInitialize()
    {
        repository = new BusinessObjectRepository(DatabasePath);
        app = new KillerApp(repository, someOtherClass);
    }

    [TestMethod]
    public void Should_convert_all_gizmo_records_to_busn_objects()
    {
        app.DoThatThing();
        Assert.AreEqual(someOtherClass.Results, /* however you're confirming */);
    }
}

但这仍然感觉非常麻烦。有两个原因,1)Repository模式最近来自getting some bad press Ayende,他知道关于Repository的一两件事。 2)你在做什么编写自己的数据访问!?使用NHibernateActiveRecord

[ActiveRecord] /* You define your database schema on the object using attributes */
public BusinessObject
{
    [PrimaryKey]
    public Int32 Id { get; set; }

    [Property]
    public String Data { get; set; }

    /* more properties */
}

public class KillerApp
{
    private ISomeOtherClass someOtherClass;

    public KillerApp(ISomeOtherClass someOtherClass)
    {
        this.someOtherClass = someOtherClass;
    }

    public void DoThatThing()
    {
        BusinessObject[] entities = BusinessObject.FindAll() /* built-in ActiveRecord call! */
        someOtherClass.HandleNewBizObjs(entities);
    }
}

[TestClass]
public class When_Doing_That_Thing : ActiveRecordTest /* setup active record for testing */
{
    private ISomeOtherClass someOtherClass = new SomeOtherClass();
    private KillerApp app;

    [TestInitialize]
    public void TestInitialize()
    {
        app = new KillerApp(someOtherClass);
    }

    [TestMethod]
    public void Should_convert_all_gizmo_records_to_busn_objects()
    {
        app.DoThatThing();
        Assert.AreEqual(someOtherClass.Results, /* however you're confirming */);
    }
}

结果是一个更小的类和一个业务对象和数据层,您可以更轻松地更改。而且您甚至不必模拟数据库调用,您可以配置和初始化ActiveRecord以使用测试数据库(内存中,甚至)。

答案 1 :(得分:1)

嗯,我认为唯一真正需要测试的是TransformIntoBizObjs,因为连接代码应该在其他地方编写/测试过。只需传递可能显示为Transform的内容,并查看是否弹出正确的内容就是您需要做的事情。

请记住测试Transform的所有用例,甚至可能不应该在函数调用中结束的可能奇怪的项目。永远不知道人们在他们的数据库中推了什么。

答案 2 :(得分:0)

控制反转(IoC)和依赖注入(DI)将大大有助于使您的代码更易于测试。有很多框架可以帮助你解决这个问题,但是为了你的目的,你不一定需要付出那么大的努力。

首先提取可能如下所示的界面:

Interface ISqlLiteConnection
{
     public IList<GizmoRecord> Query(...);

}

完成后,您应该重构OpenSqlLiteConnection()方法以返回ISqlLiteConnection的实例,而不是具体的实现。要进行测试,只需创建一个实现您的接口的类,它可以模拟实际的数据库查询和具有确定结果的连接。

答案 3 :(得分:-1)

数据库很复杂,你需要测试你的查询代码,你需要针对一个真正的sqlite实例进行测试 - 否则你不能确定你没有遇到一些罕见的sqlite怪癖或bug。

并且由于测试查询的唯一方法是在真正的sqlite文件上运行它,并且在测试中包含这样的文件非常容易,添加另一层只是为了使它“更”可测试或者进行“纯粹的”单元测试。

只需确保将您能想到的所有奇怪边缘情况添加到示例文件中。