分享
 
 
 

设计自己的DbUnit

王朝other·作者佚名  2008-05-19
窄屏简体版  字體: |||超大  

在数据库代码测试中,一般情况使用2种方案:

一是使用mock objects;

二是使用DbUnit。

mock objects基于物理隔离层的概念,将涉及到数据库操作的代码,全用虚拟对象代替。这种方案,对业务领域里的代码来讲是可行的,也比较方便,但对于数据库操作层,此方案无用武之地,因为我们必须实实在在地与数据库打交道。

而在数据库测试中,因为我们力求将每个TestCase中众多的测试方法完全隔离起来,不会因为一个测试方法因测试增加、删除功能而影响到另一个测试方法,这样,在每一个测试之前,数据库的状态是否稳定,甚至是完全不变,就显得很重要了。而这点,正是数据库测试的难点。

Dbunit解决了这个问题。其原理很简单,就是在每个测试方法之前后,通过增删一些固定的记录,保持了数据库的固定状态,由此,我们可以在每个测试方法中自由地增删记录,而不用担心会影响到别的测试方法。

但Dbunit也有一个问题,即它不能删除非空的外键记录。举例来说,假设“员工”表中有一非空字段为“部门编号”,引用了“部门”表的id, 只要“员工”表存在任一记录,“部门”表将不能被删除,强行删除将出现违犯约束(constraint violation)的异常。当然,如果必要,我们可以将数据库的约束条件改为连锁删除,这样,一旦我们删除一名员工记录,其所在的部门记录也将从“部门”表中删除。而此又会导致“员工”表中所有该部门的员工全被删除。这是绝对不允许的。当然,作为测试,我们可以先删除“员工”表,再删除“部门”表。

但有时,某些表自己引用自己,如“组织”表中有一“上级组织编号”字段,是自己“组织编号”的外键,即,此字段引用了本表中其他记录的“组织编号”。此时,我们必须先将这些引用了其他记录的“组织编号”的记录先删除,才能删除此表中的其他记录。而Dbunit在实现上,只是用了一个简单的"delete from ..."的SQL语句,不能解决这个问题。

Dbunit的原理是如此简单,我们完全可以设计的“Dbunit”,通过多重循环语句,干脆利落地删除自引用的整表。我们的“Dbunit”,可以命名为“SqlRunner”。

package com.sarkuya.util.database;

import java.sql.Connection;

import java.sql.DriverManager;

import java.sql.ResultSet;

import java.sql.SQLException;

import java.sql.Statement;

public class SqlRunner {

static {

try {

Class.forName("org.hsqldb.jdbcDriver");

} catch (ClassNotFoundException ex) {

ex.printStackTrace();

}

}

public static void executeUpdate(String sql) {

Connection conn;

Statement stmt;

try {

conn = DriverManager.getConnection("jdbc:hsqldb:mem:testingdb", "sa", "");

stmt = conn.createStatement();

stmt.executeUpdate(sql);

stmt.close();

conn.close();

} catch (SQLException ex) {

ex.printStackTrace();

}

}

public static boolean isUndeletableForSelfReference (String 表名, String 字段名) {

Connection conn;

Statement stmt;

boolean result = true;

try {

conn = DriverManager.getConnection("jdbc:hsqldb:mem:testingdb", "sa", "");

stmt = conn.createStatement();

ResultSet rs = stmt.executeQuery("select count(*) from " + 表名 + " where " + 字段名 + " is not null");

rs.next();

if (rs.getInt(1) != 0) {

result = true;

}

else {

result = false;

}

rs.close();

stmt.close();

conn.close();

} catch (SQLException ex) {

ex.printStackTrace();

}

return result;

}

可以看出,我们使用了JDBC的SQL语句,而不是Hibernate语句。Hibernate的粉丝们可能大为不满,为何不使用Hiberante? 别急,Hibernate的语句将被大量地应用于实际测试当中。但是根据测试先行的原则,任何一个基于Hiberante的语句都必须先测试再使用。而我们的这个“Dbunit”是运行在实际测试之前,无法经过测试。当然,我们可以先假定这段Hiberante代码正确无误,然后再实际测试它。这种方法也有一个缺点,因为测试代码常常会因为重构而发生改变,当测试代码改变时,这个“Dbunit”也将被迫发生改变。而用JDBC的SQL语句,可保持这段代码相对独立,不至于连诛九族。

executeUpdate()将执行“insert”、“delete”语句。重点在于isUndeletableForSelfReference()方法。此方法在某个表的某个字段非空时,会返回false,告诉我们,此表中尚有被引用的记录存在,从而不能删除此表。尽管只有两个方法,但对于我们的“Dbunit”来讲,已经足够了。

在TestCase的setUp()中,我们利用其executeUpdate来增加一些必须的记录。

protected void setUp() throws Exception {

SqlRunner.executeUpdate("insert into 组织分类 values(1, '教育系统')");

SqlRunner.executeUpdate("insert into 组织分类 values(2, '商贸系统')");

SqlRunner.executeUpdate("insert into 组织分类 values(3, '供应商家')");

SqlRunner.executeUpdate("insert into 组织分类 values(4, '政府')");

SqlRunner.executeUpdate("insert into 组织 values(1, '中国贸易部', '北京三环路558号', 2, null)");

SqlRunner.executeUpdate("insert into 组织 values(2, '北京贸易厅', '北京四环路8号', 2, 1)");

SqlRunner.executeUpdate("insert into 组织 values(3, '河北高科技技术服务有限公司', '石家庄市白龙路23号', 3, null)");

SqlRunner.executeUpdate("insert into 组织 values(4, '四川珠宝有限公司', '成都市蓝天路56号', 3, null)");

SqlRunner.executeUpdate("insert into 组织 values(5, '北京昌平贸易局', '北京五环路18号', 2, 2)");

SqlRunner.executeUpdate("insert into 部门 values(1, '财务科', 2)");

SqlRunner.executeUpdate("insert into 部门 values(2, '市场部', 2)");

SqlRunner.executeUpdate("insert into 部门 values(3, '人事部', 2)");

}

其中,“组织”表的结构为:

编号(bigint),名称(varchar),地址(varchar),组织分类编号(bigint),上级组织编号(bigint)

“部门”表的结构为:

编号(bigint),名称(varchar),地址(varchar),组织编号(bigint)

在“组织”表中,编号为5的记录引用了2的记录,2的记录引用了1的记录。

而在tearDown()中,我们配合isUndeletableForSelfReference()来删除相应记录。

protected void tearDown() throws Exception {

SqlRunner.executeUpdate("delete from 部门");

while (SqlRunner.isUndeletableForSelfReference("组织", "上级组织编号")) {

SqlRunner.executeUpdate("delete from 组织 where 上级组织编号 is not null and 编号 not in (select 上级组织编号 from 组织 where 上级组织编号 is not null)");

}

SqlRunner.executeUpdate("delete from 组织");

SqlRunner.executeUpdate("delete from 组织分类");

}

因为“部门”引用“组织”,“组织”引用“组织分类”,因此我们必须依序删除“部门”、“组织”及“组织分类”。难点在于while语句,其人工语义是,只要“组织”表中存在引用了其他记录的“编号”的记录,就会返回true,就先将这些引用的记录删除;只要“组织”表中不再有被引用的记录了,我们可以安全地用“delete from 组织”删除它们。

而在测试代码中,在任何一个测试方法中,我们可以直接使用如下语句:

assertEquals(5, 组织Service.get组织数量());

对于数据库测试代码来讲,速度是摆在第一位的,因此我们选择了Hsqldb的内存数据库方式。这种方式不能永久保存记录,但只有测试期间,数据可用就行了。本人的实际测试代码中,某个TestCase,共有28个测试方法,代码将近千行,测试速度不到8秒,基本可以忍受。主要瓶颈在于setUp()及tearDown()总共运行了28遍。当然,setUp()中插入的数据越少,测试速度就越快,但每个测试方法中可能就需要增加一些工作量了。取舍完全在于你自己。

作者:Sarkuya(作者的blog:http://blog.matrix.org.cn/page/Sarkuya)

原文:http://blog.matrix.org.cn/page/Sarkuya?entry=%E8%AE%BE%E8%AE%A1%E8%87%AA%E5%B7%B1%E7%9A%84dbunit

 
 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
2023年上半年GDP全球前十五强
 百态   2023-10-24
美众议院议长启动对拜登的弹劾调查
 百态   2023-09-13
上海、济南、武汉等多地出现不明坠落物
 探索   2023-09-06
印度或要将国名改为“巴拉特”
 百态   2023-09-06
男子为女友送行,买票不登机被捕
 百态   2023-08-20
手机地震预警功能怎么开?
 干货   2023-08-06
女子4年卖2套房花700多万做美容:不但没变美脸,面部还出现变形
 百态   2023-08-04
住户一楼被水淹 还冲来8头猪
 百态   2023-07-31
女子体内爬出大量瓜子状活虫
 百态   2023-07-25
地球连续35年收到神秘规律性信号,网友:不要回答!
 探索   2023-07-21
全球镓价格本周大涨27%
 探索   2023-07-09
钱都流向了那些不缺钱的人,苦都留给了能吃苦的人
 探索   2023-07-02
倩女手游刀客魅者强控制(强混乱强眩晕强睡眠)和对应控制抗性的关系
 百态   2020-08-20
美国5月9日最新疫情:美国确诊人数突破131万
 百态   2020-05-09
荷兰政府宣布将集体辞职
 干货   2020-04-30
倩女幽魂手游师徒任务情义春秋猜成语答案逍遥观:鹏程万里
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案神机营:射石饮羽
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案昆仑山:拔刀相助
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案天工阁:鬼斧神工
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案丝路古道:单枪匹马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:与虎谋皮
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:李代桃僵
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:指鹿为马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:小鸟依人
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:千金买邻
 干货   2019-11-12
 
推荐阅读
 
 
 
>>返回首頁<<
 
靜靜地坐在廢墟上,四周的荒凉一望無際,忽然覺得,淒涼也很美
© 2005- 王朝網路 版權所有