主要依賴
- Spring : 4.3.12.RELEASE
- mybatis : 3.4.5
- junit : 4.12
- hamcrest-all : 1.3
- h2 : 1.4.196
- DbSetup : 2.1.0
DAO 層的單元測試應該怎麼做
如果按照單元測試的定義,我們的不應該依賴於如數據庫、網絡、文件等,使用 Mockito 這類框架可以模擬這些外界因素。但是對於 DAO 層來說,脫離了數據庫,單元測試還能發揮它應有的作用嗎?
看下面使用 Mockito 的例子
@Test
public void testAdd() {
Student student = new Student();
StudentDao studentDao = mock(StudentDao.class);
studentDao.add(student);
verify(studentDao).add(student);
}
說實話,上面的測試沒有任何作用,即使這個測試通過,它甚至不能保證這個DAO 所執行的 SQL 能在數據庫中執行。DAO 層跟數據庫耦合的是如此的緊密,以至於脫離數據庫的測試沒有一點意義。其實我們對 DAO 層的測試是爲了兩點:
- 它執行了正確的 SQL 嗎?
- 它返回了我們所需要的數據嗎?
這兩點都是需要數據庫環境支持的。那麼我們需要連接本地的數據庫進行測試嗎?比如說連接本地的 MySQL ? 不,這樣會使得測試過於依賴運行的電腦環境,如果別的同事 clone 了這個項目,他還需要搭建測試用的數據庫,這很麻煩。所以我們使用內存數據庫,像我這邊是用 h2 數據庫,這樣我們只需添加依賴包即可。
如此看來這樣的測試已經不能叫單元測試了,更像是集成測試?
哪些是需要測試的?
在我看來,除了最基本的 CURD,其它都測。因爲 CURD 通常都是代碼生成器,像 mybatis-generator 這樣的工具生成的,測試這些生成的 SQL 就像測試第三方的類一樣意義不大。只需要測試自己寫的方法就好。
Spring Test
Spring 本身就有一套測試框架,很容易跟 Junit 整合。這裏的例子使用 Maven 作爲構建工具。
測試用到的依賴
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-all</artifactId>
<version>1.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${spring.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.4.196</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.ninja-squad</groupId>
<artifactId>DbSetup</artifactId>
<version>2.1.0</version>
<scope>test</scope>
</dependency>
測試目錄結構
在 Spring 的測試配置文件 application-context.xml 中,使用 h2 數據庫作爲數據源
<jdbc:embedded-database id="dataSource" type="H2" generate-name="true"/>
//初始化數據庫
<jdbc:initialize-database>
<jdbc:script location="schema.sql" encoding="UTF-8"/>
</jdbc:initialize-database>
這裏我們有一個初始化數據庫的文件 schema.sql
,用來建立使用到的表。本例使用的表有 student、clazz
create table student (
id int not null auto_increment primary key,
name varchar(225),
clazz_id int
);
create table clazz (
id int not null auto_increment primary key,
name varchar(225),
grade tinyint not null
)
假設我們有一個 StudentDao
類,有一個方法 List<Student> findByGrade(Long grade)
,根據年級查找學生,我們建立相應的測試類
@RunWith(SpringJUnit4ClassRunner.class)
使用 Spring 提供的 runner。@ContextConfiguration("classpath:application-context.xml")
指定 Spring 配置文件
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:application-context.xml")
public class StudentDaoTest {
@Test
public void testFindByGrade() {
}
}
好了,現在我們有了一個內存數據庫,表也有了,那就還差數據了。當然我們可以在初始化數據庫表的時候將測試數據插入,但相對於每一個測試來說,數據不夠細,且這些數據不好維護。我更傾向於在測試類中初始化相應的數據。我一開始看了下 DbUnit,它是使用 XML 來構造數據的,說實話,XML 也不好維護。後來發現了 DbSetup,感覺好用一點,雖然他們的功能是不完全相同的,對於測試有對數據庫進行數據修改,如更新、插入、刪除等方法,DbUnit還是比較在行。DbSetup 主要是關注於數據的初始化。具體的用法這些就自己去看了,這裏給個簡單的例子
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:application-context.xml")
public class StudentDaoTest {
private static DbSetupTracker dbSetupTracker = new DbSetupTracker();
@Autowired DataSource dataSource;
@Autowired StudentDao studentDao;
@Before
public void setUp() {
Operation operation = Operations.sequenceOf(
Operations.deleteAllFrom("student", "clazz"),
Operations.insertInto("student")
.columns("id", "name", "clazz_id")
.values(1L, "Tom", 1L)
.values(2L, "Jack", 2L)
.values(3L, "Red", 1L)
.build(),
Operations.insertInto("clazz")
.columns("id", "name", "grade")
.values(1L, "1-A", 1L)
.values(2L, "2-B", 2L)
.build()
);
DbSetup dbSetup = new DbSetup(new DataSourceDestination(dataSource), operation);
dbSetupTracker.launchIfNecessary(dbSetup);
}
@Test
public void testFindByGrade() {
dbSetupTracker.skipNextLaunch();
List<Student> actual = studentDao.findByGrade(1L);
assertThat(actual, hasSize(2));
assertThat(actual, hasItems(
hasProperty("id", is(1L)),
hasProperty("id", is(2L))
));
}
}
在 setUp
方法中,即使沒有看過 DbSetup 的文檔,相信你也很容易就看懂它是如何初始化數據的。然後在 testFindByGrade
方法中,就是典型的斷言了。這裏我只是對 Student 的 id 進行了比較,當然你可以對比所有的屬性。