我如何为我的单元测试实现/构建/创建'内存数据库'

时间:2010-04-29 10:56:52

标签: asp.net-mvc database unit-testing

我刚刚开始进行单元测试,结果我做了比单元测试更多的回归测试,因为我还包括了我的数据库层,因此非常时间进入数据库。

所以,实现Unity注入假数据库层,但我当然想存储一些数据,主要意见是:“创建一个内存数据库”

但那是什么/我该如何实现呢?

主要问题是:我认为我必须伪造数据库层,但这并不能让我自己创建一个“简单数据库”或者:我如何保持简单而不是仅为我的单元测试重建Sql Server: )

在这个问题的最后,我将解释我刚刚开始的项目所涉及的情况,我想知道这是否可行。

米歇尔

我在这个客户端看到的当前情况是,testdata包含在XML文件中,并且有一个“假”数据库层将所有xml文件连接在一起。 对于真正的数据库,我们使用实体框架,这非常简单。 而现在,在'假'层,我有顶级创建所有类加载,保存,持久等数据。 听起来很奇怪,虚假层中有太多的工作,而真实层中的工作却很少。

我希望这一切都有意义:)

编辑: 所以我知道我必须为我的单元测试创​​建一个单独的数据库层,但我该如何实现呢?

5 个答案:

答案 0 :(得分:3)

为您的数据访问层定义接口并且(至少)有两个实现:

  • 真正的数据库提供程序,它将依次在SQL数据库上运行查询等。
  • 内存测试提供程序,可以在每个单元测试中预先填充测试数据。

这样做的好处是,使用数据提供者的模块不需要数据库是真实数据库还是测试数据库,因此将测试更多真实代码。测试数据库可以是简单的(如简单的对象集合)或复杂的(具有索引的自定义结构)。它也可以是一个模拟的实现,它将断言它作为测试的一部分被恰当地调用。

此外,如果您需要支持其他数据存储方法(或不同的SQL数据库),您只需要编写符合该接口的另一个实现,并且可以确信所有调用代码都不需要重新编写

如果您从一开始(或接近)开始计划,这种方法最简单,所以我不确定应用于您的情况会有多容易。

它可能是什么样的

如果您只是通过id加载和保存对象,那么您可以拥有一个接口和实现(在Java-esque伪代码中;我对asp.net不太了解):

interface WidgetDatabase {
    Widget loadWidget(int id);
    saveWidget(Widget w);
    deleteWidget(int id);
}

class SqlWidgetDatabase extends WidgetDatabase {
    Connection conn;

    // connect to database server of choice
    SqlWidgetDatabase(String connectionString) { conn = new Connection(connectionString); }

    Widget loadWidget(int id) {
        conn.executeQuery("SELECT * FROM widgets WHERE id = " + id);
        Widget w = conn.fetchOne();
        return w;
    }

    // more methods that run simple sql queries...
}

class MemeoryWidgetDatabase extends WidgetDatabase {
    Set widgets;

    MemoryWidgetDatabase() { widgets = new Set(); }

    Widget loadWidget(int id) {
        for (Widget w: widgets)
            if (w.getId() == id)
                return w;
        return null;
    }

    // more methods that find/add/delete a widget in the "widgets" set...
}

如果您需要运行更多其他查询(例如基于更复杂条件的批量选择),您可以添加方法来执行此操作。

同样适用于复杂的更新。事务支持可用于真正的数据库实现。我不确定构建一个能够提供适当事务支持的内存数据库是多么容易。要测试它,您需要“打开”到同一数据集的几个“连接”,并且只在提交事务时将更新应用于该共享数据集。

答案 1 :(得分:2)

我使用Sqlite作为假数据库进行单元测试

答案 2 :(得分:2)

为什么不使用模拟框架(如moq或rhino模拟)?如果通过接口访问数据,则可以模拟该接口并指定每次测试时要返回的内容。其他方法是使用单独的环境进行测试,使用“真实”数据库,在为生产环境获取代码之前进行测试。

答案 3 :(得分:1)

我一直在内存中使用 Sqlite进行单元测试,这非常有用

答案 4 :(得分:1)

嗯......如果您将所有测试数据存储在XML文件中。您刚刚将一个数据库更改为另一个数据库。这不是内存数据库中的 。在PHP中你会使用这样的东西。

class MemoryProductDB {

    private $products;

    function MemoryProductDB() {
        $this->products = array();
    }

    public function find($index) {
        return $this->products[$index];
    }

    public function save($product) {
        $this->products[$product['index']] = $product;
    }
}

您注意到我的所有数据都存储在内存数组中,并从内存数组中检索。这是一个简单的内存数据库

恕我直言,如果您使用XML来存储测试数据,那么您确实没有有效地断开模型和数据库的依赖关系。无论您的业务规则有多复杂,当他们触摸数据库时,他们真正做的就是CRUD(创建,检索,更新和删除)功能。

如果您在模型中处理的是数据库中的多个对象,那么您可能需要将所有这些对象组合成单个对象并让模型使用该对象。一个例子是由产品组成的order。不要检索产品然后保存产品。检索订单然后保存订单并让您的模型按订单运行。该模型不应该对产品有任何了解。

这称为抽象粒度。

[编辑] 评论中有一个非常好的问题。使用内存数据库进行测试时,我们不关心选择如何在数据库中工作。首先,控制器必须具有数据库上的功能,以计算可以访问以进行分页的可能记录的数量。 IMDb(在内存数据库中)应该只发送一个数字。控制器永远不应该关心那个号码是什么。与实际记录相同。希望你的控制器正在做的是显示从IMDb返回的内容。

[编辑] 您永远不应该使用实时模型和imdb对您的控制器进行单元测试。 imdb的设置代码会有很多摩擦。相反,当单元测试控制器时,您需要对模拟,存根,假模型进行单元测试。 imdb的最佳用途是在集成测试期间或单元测试模型时。 imdb不是假的吗?

我的情景是:

  1. 在我的客户端,我使用插件进行表格。 DataTables。服务器端处理。
  2. 客户端GET请求表product.get(5,10)中的项目。返回数据将以JSON编码。
  3. 该模型将负责通过从网关到数据库检索信息来形成JSON。网关只是数据库的一个外观。我是一个嘲弄者,所以我的网关是模拟而不是内存网关。

    public function testSkuTable() {
        $skus = array(
                array('id' => '1', 'data' => 'data1'),
                array('id' => '2', 'data' => 'data2'),
                array('id' => '3', 'data' => 'data3'));
    
        $names = array(
                'id',
                'data');
        $start_row = $this->parameters['start_row'];
        $num_rows = $this->parameters['num_rows'];
        $sort_col = $this->parameters['sort_col'];
        $search = $this->parameters['search'];
        $requestSequence = $this->parameters['request_sequence'];
        $direction = $this->parameters['dir'];
        $filterTotals = 1;
        $totalRecords = 1;
    
        $this->gateway->expects($this->once())
                ->method('names')
                ->with($this->vendor)
                ->will($this->returnValue($names));
    
        $this->gateway->expects($this->once())
                ->method('skus')
                ->with($this->vendor, $names, $start_row, $num_rows, $sort_col, $search, $direction)
                ->will($this->returnValue($skus));
    
        $this->gateway->expects($this->once())
                ->method('filterTotals')
                ->will($this->returnValue($filterTotals));
    
        $this->gateway->expects($this->once())
                ->method('totalRecords')
                ->with($this->vendor)
                ->will($this->returnValue($totalRecords));
    
        $expectJson = '{"sEcho": '.$requestSequence.', "iTotalRecords": '.$totalRecords.', "iTotalDisplayRecords": '.$filterTotals.', "aaData": [ ["1","data1"],["2","data2"],["3","data3"]] }';
        $actualJson = $this->skusModel->skuTable($this->vendor, $this->parameters);
    
        $this->assertEquals($expectJson, $actualJson);
    }
    

    你会注意到,通过这个单元测试,我不关心数据是什么样的。 $skus甚至看起来都不像实际的表模式。只是我返回记录。以下是该模型的实际代码:

    public function skuTable($vendor, $parameterList) {
        $startRow = $parameterList['start_row'];
        $numRows = $parameterList['num_rows'];
        $sortCols = $parameterList['sort_col'];
        $search = $parameterList['search'];
        if($search == null) {
            $search = "";
        }
        $requestSequence = $parameterList['request_sequence'];
        $direction = $parameterList['dir'];
    
        $names = $this->propertyNames($vendor);
        $skus = $this->skusList($vendor, $names, $startRow, $numRows, $sortCols, $search, $direction);
        $filterTotals = $this->filterTotals($vendor, $names, $startRow, $numRows, $sortCols, $search, $direction);
        $totalRecords = $this->totalRecords($vendor);
    
        return $this->buildJson($requestSequence, $totalRecords, $filterTotals, $skus, $names);
    }
    

    该方法的第一部分打破了我从get请求获得的$parameterList中的各个参数。其余的是对网关的调用。以下是其中一种方法:

    public function skusList($vendor, $names, $start_row, $num_rows, $sort_col, $search, $direction) {
        return $this->skusGateway->skus($vendor, $names, $start_row, $num_rows, $sort_col, $search, $direction);
    }