2

我的任务是从第三方供应商的 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

4 回答 4

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

使用 anIRepository会从这个类中删除一些代码,允许你模拟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)最近Ayende对 Repository 模式进行了一些负面 报道,他对 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 以使用测试数据库(甚至在内存中)。

于 2009-06-04T20:47:01.310 回答
1

好吧,我认为唯一真正需要在这里测试的是 TransformIntoBizObjs,因为连接代码应该在其他地方编写/测试。只需将可能出现的内容传递给 Transform 并查看是否弹出正确的内容就是您需要做的。

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

于 2009-06-04T19:51:38.117 回答
0

控制反转 (IoC) 和依赖注入 (DI) 将大大有助于使您的代码更具可测试性。有很多框架可以帮助您解决此问题,但出于您的目的,您不一定需要付出所有努力。

从提取可能看起来像这样的界面开始:

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

}

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

于 2009-06-04T20:35:23.503 回答
-1

数据库很复杂,您需要测试您的查询代码,并且您需要针对真实的 sqlite 实例对其进行测试 - 否则您无法确定您没有遇到一些罕见的 sqlite 怪癖或错误。

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

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

于 2009-06-04T20:19:59.353 回答