五个方法让你做更好的java单元测试?

五个方法让你做更好的java单元测试?,第1张

单元测试是我们在软件开发过程中经常用到的一种软件测试的方法,而今天我们就一起来了解一下,一个好的单元测试都是如何来编辑完成的。

1.使用框架来用于单元测试

Java提供了若干用于单元测试的框架。TestNG和JUnit是流行的测试框架。JUnit和TestNG的一些重要功能:

易于设置和运行。

支持注释。

允许忽略或分组并一起执行某些测试。

支持参数化测试,即通过在运行时指定不同的值来运行单元测试。

通过与构建工具,如Ant,Maven和Gradle集成来支持敬脊自动化的测兆稿蔽试执行。

EasyMock是一个模拟框架,是单元测试框架,如JUnit和TestNG的补充。EasyMock本身不是一个完整的框架。它只是添加了创建模拟对象以便于测试的能力。例如,我们想要测试的一个方法可以调用从数据库获取数据的DAO类。在这种情况下,EasyMock可用于创建返回硬编码数据的MockDAO。这使我们能够轻松地测试我们意向的方法,而不必担心数据库访问。

2.谨慎使用测试驱动开发!

测试驱动开发(TDD)是一个软件开发过程,在这过程中,在开始任何编码之前,我们基于需求来编写测试。由于还没有编码,测试初会失败。然后写入小量的代码以通过测试。然后重构代码,直到被优化。

目标是编写覆盖所有需求的测试,而不是一开始就写代码,却可能甚至都不能满足需求。TDD是伟大的,因为它导致简单的模块化代码,且易于维护。总体开发速度加快,容易发现缺陷。此外,单元测试被创建作为TDD方法的副产品。

然而,TDD可能不适合所有的情况。在设计复杂的项目中,专注于简单的设计以便于通过测试用例,而不提前思考可能会导致巨大的代码更改。此外,TDD方法难以用于与遗留系统,GUI应用程序或与数据库一起工作的应用程序交互的系统。另外,测试需要随着代码的改变而更新。

因此,在决定采用TDD方法之前,应考虑上述因素,并应根据项目的性质采取措施。

3.测量代码覆盖率

代码覆盖率衡量(以百分比表族州示)了在运行单元测试时执行的代码量。通常,高覆盖率的代码包含未检测到的错误的几率要低,因为其更多的源代码在测试过程中被执行。天通苑电脑培训发现测量代码覆盖率的一些佳做法包括:

使用代码覆盖工具,如Clover,Corbetura,JaCoCo或Sonar。使用工具可以提高测试质量,因为这些工具可以指出未经测试的代码区域,让你能够开发开发额外的测试来覆盖这些领域。

1. 单元测试的编写原则

Junit 附带文档所列举的单元测试带有一定的迷惑性,因为几乎所有的示例单元都是针对某个对象的某个方法,似乎 Junit 的单元测试仅适用于类组织结构的静态约束,从而使初学者怀疑 Junit 下的单元测试所能带来的效果。因此我们需要重新定义如何确凯旦定有价值的单元测试以及如何编写这些单元测试、维护这些单元测试,从而让更多的程序员接受和熟悉 Junit 下的单元测试的编写。

在 Junit 单元测试框架的设计时,作者一共设定了三个总体目标,第一个是简化测试的编写,这种简化包括测试框架的学习和实际测试单元的编写;第二个是使测试单元保持持久性;第三个则是可以利用既有的测试来编写相关的测试。从这三个目标可以看出,单元测试框架的基本设计考虑依然是从我们现有的测试方式和方法出发,而只是使测试变得更加容易实施和扩展并保持持久性。因此编写单元测试的原则可以从我们通常使用的测试方法借鉴和利用。

回页首

2. 如何确定单元测试

在我们通常的测试中,一个单元测试一般针对于特定对象的一个特定特性,譬如,假定我们编写了一个针对特定数据库访问的连接池的类包实现,我们会建立以下的单元测试:

在连接池启动后,是否根据定义的规则在池中建立了相应数量的数据库连接

申请一个数据库连接,是否根据定义的规则从如孙羡池中直接获得缓存连接的引用,还是建立新的连接

释放一个数据库连接后,连接是否根据定义的规则被池释放或者缓存以便以后使用

后台 Housekeeping 线程是否按照定义的规则释放已经过期的连接申请

如果连接有时间期限,后台 Housekeeping 线程是否定期释放已经过期的缓存连接

这儿只列出了部分的可能测试,但是从这个列表我们可以看出单元测试的粒度。一个单元测试基本是以一个对象的明确特性为基础,单元测试的过程应该限定在一个明确的线程范围内。根据上面所述,一个单元测试的测试过程非常类似于一个 Use Case 的定义,但是单元测试的粒度一般来说比 Use Case 的定义要小,这点是容易理解的,因为 Use Case 是以单独的事务单元为基础的,而单元测试是以一组聚合性很强的对象的特定特征为基础的,一般而言一个事务中会利用许多的系统特征来完成具体的软件需求。

从上面的分析我们可以得出,测试单元应该以一个对象的内部状态的转换为基本编写单元。一个软件系统就和一辆设计好的汽车一样,系统的状态是由同一时刻时系统内部的各个分立的部件的状态决定的,因此为了确定一个系统最终的行为符合我们起始的要求,我们首先需要保证系统内的各个部分的状态会符合我们的设计要求,所以我们的测试单元的重点应该放在确定对象的状态变换上。

然而需要注意的并不是所有的对象组特征都需要被编写成独立的测试单元,如何在对象组特征里筛选有价值的测试单元的原则在 JUnitTest Infected: Programmers Love Writing Tests 一文中得到了正确的描述,你应该在有可能引入错误的地方引入测试单元,通常这些地方存在于有特定边界条件、复杂算法以及需求变动比较频繁的代码逻辑中。除了这些特性需要被编写成独立的测试单元外,还有一些边界条件比较复杂的对象方法也应该被编写成独立的测试单元,这部分单元测试已经在 Junit 文档中被较好的描述和解释过了。

在基本确定了需要编写的单元测试,我们还应该问自己:编写好了这些测试,我们是否可以有把握地告诉自己,如果代码通过了这些单元测试,我们能认定程序的运行是正确的,符合需求的。如果我们不能非常的确定,就应该看看是否还有遗漏的需要编写的单元测试或者重新审视我们对软件需求的理解。通常来说,在开始使用单元测试的时候,更多的单元测试总是没有错的。

一旦我们确定了需要被编写的测试单元,接下来就应该

回页首

3. 如何编写单元测试

在 XP 下强调单元测试必须由类包的编写者负责编写,这个限定对于我们渣拍设定的测试目标是必须的。因为只有这样,测试才能保证对象的运行时态行为符合需求,而仅通过类接口的测试,我们只能确保对象符合静态约束,因此这就要求我们在测试的过程中,必须开放一定的内部数据结构,或者针对特定的运行行为建立适当的数据记录,并把这些数据暴露给特定的测试单元。这也就是说我们在编写单元测试时必须对相应的类包进行修改,这样的修改也发生在我们以前使用的测试方法中,因此以前的测试标记及其他一些测试技巧仍然可以在 Junit 测试中改进使用。

由于单元测试的总体目标是负责我们的软件在运行过程中的正确无误,因此在我们对一个对象编写单元测试的时候,我们不但需要保证类的静态约束符合我们的设计意图,而且需要保证对象在特定的条件下的运行状态符合我们的预先设定。还是拿数据库缓冲池的例子说明,一个缓冲池暴露给其他对象的是一组使用接口,其中包括对池的参数设定、池的初始化、池的销毁、从这个池里获得一个数据连接以及释放连接到池中,对其他对象而言随着各种条件的触发而引起池的内部状态的变化是不需要知道的,这一点也是符合封装原理的。但是池对象的状态变化,譬如:缓存的连接数在某些条件下会增长,一个连接在足够长的运行后需要被彻底释放从而使池的连接被更新等等,虽然外部对象不需要明确,但是却是程序运行正确的保证,所以我们的单元测试必须保证这些内部逻辑被正确的运行。

编译语言的测试和调试是很难对运行的逻辑过程进行跟踪的,但是我们知道,无论逻辑怎么运行,如果状态的转换符合我们的行为设定,那验证结果显然是正确的,因此在对一个对象进行单元测试的时候,我们需要对多数的状态转换进行分析和对照,从而验证对象的行为。状态是通过一系列的状态数据来描述的,因此编写单元测试首先分析出状态的变化过程(状态转换图对这个过程的描述非常清晰),然后根据状态的定义确定分析的状态数据,最后是提供这些内部的状态数据的访问。在数据库连接池的例子中,我们对池实现的对象 DefaultConnectionProxy 的状态变换进行分析后,我们决定把表征状态的 OracleConnectionCacheImpl 对象公开给测试类。参见示例一

示例一

/**

* 这个类简单的包装了 oracle 对数据连接缓冲池的实现。

*

*/

public class DefaultConnectionProxy extends ConnectionProxy {

private static final String name = "Default Connection Proxy"

private static final String description =

"这个类简单的包装了 oracle 对数据连接缓冲池的实现。"

private static final String author = "Ion-Global.com"

private static final int major_version = 0

private static final int minor_version = 9

private static final boolean pooled = true

private ConnectionBroker connectionBroker = null

private Properties props

private Properties propDescriptions

private Object initLock = new Object()

// Test Code Begin...

/* 为了能够了解对象的状态变化,因此需要把表征对象内部状态变化的部分私有变量提

供公共的访问接口(或者提供让同一个类包的访问接口),以便使测试单元可以有效地

判断对象的状态转变,在本示例中对包装的 OracleConnectionCacheImpl 对象提供访问

接口。

*/

OracleConnectionCacheImpl getConnectionCache() {

if (connectionBroker == null) {

throw new IllegalStateException("You need start the server first.")

}

return connectionBroker.getConnectionCache()

}

// Test Code End...

在公开内部状态数据後,我们就可以编写我们的测试单元了,单元测试的选择方法和选择尺度已经在本文前面章节进行了说明,但是仍然需要注意的是,由于 assert 方法会抛出一个 error,你应该在测试方法的最后集中用 assert 相关方法进行判断,这样可以确保资源得到释放。

对数据库连接池的例子,我们可以建立测试类 DefaultConnectionProxyTest,同时建立数个 test case,如下:

示例二

/**

* 这个类对示例一中的类进行简单的测试。

*

*/

public class DefaultConnectionProxyTest extends TestCase {

private DefaultConnectionProxy conProxy = null

private OracleConnectionCacheImpl cacheImpl = null

private Connection con = null

/** 设置测试的 fixture,建立必要的测试起始环境。

*/

protected void setUp() {

conProxy = new DefaultConnectionProxy()

conProxy.start()

cacheImpl = conProxy.getConnectionCache()

}

/** 对示例一中的对象进行服务启动后的状态测试,检查是否在服务启动后,

连接池的参数设置是否正确。

*/

public void testConnectionProxyStart() {

int minConnections = 0

int maxConnections = 0

assertNotNull(cacheImpl)

try {

minConnections = Integer.parseInt(PropertyManager.getProperty

("DefaultConnectionProxy.minConnections"))

maxConnections = Integer.parseInt(PropertyManager.getProperty

("DefaultConnectionProxy.maxConnections"))

} catch (Exception e) {

// ignore the exception

}

assertEquals(cacheImpl.getMinLimit(), minConnections)

assertEquals(cacheImpl.getMaxLimit(), maxConnections)

assertEquals(cacheImpl.getCacheSize(), minConnections)

}

/** 对示例一中的对象进行获取数据库连接的测试,看看是否可以获取有效的数据库连接,

并且看看获取连接后,连接池的状态是否按照既定的策略进行变化。由于 assert 方法抛出的是

error 对象,因此尽可能把 assert 方法放置到方法的最后集体进行测试,这样在方法内打开的

资源,才能有效的被正确关闭。

*/

public void testGetConnection() {

int cacheSize = cacheImpl.getCacheSize()

int activeSize = cacheImpl.getActiveSize()

int cacheSizeAfter = 0

int activeSizeAfter = 0

con = conProxy.getConnection()

if (con != null) {

activeSizeAfter = cacheImpl.getActiveSize()

cacheSizeAfter = cacheImpl.getCacheSize()

try {

con.close()

} catch (SQLException e) {

}

} else {

assertNotNull(con)

}

/* 如果连接池中的实际使用连接数小于缓存连接数,检查获取的新的数据连接是否

从缓存中获取,反之连接池是否建立新的连接

*/

if (cacheSize >activeSize) {

assertEquals(activeSize + 1, activeSizeAfter)

assertEquals(cacheSize, cacheSizeAfter)

} else {

assertEquals(activeSize + 1, cacheSizeAfter)

}

}

/** 对示例一中的对象进行数据库连接释放的测试,看看连接释放后,连接池的

状态是否按照既定的策略进行变化。由于 assert 方法抛出的是 error 对象,因此尽可

能把 assert 方法放置到方法的最后集体进行测试,这样在方法内打开的

资源,才能有效的被正确关闭。

*/

public void testConnectionClose() {

int minConnections = cacheImpl.getMinLimit()

int cacheSize = 0

int activeSize = 0

int cacheSizeAfter = 0

int activeSizeAfter = 0

con = conProxy.getConnection()

if (con != null) {

cacheSize = cacheImpl.getCacheSize()

activeSize = cacheImpl.getActiveSize()

try {

con.close()

} catch (SQLException e) {

}

activeSizeAfter = cacheImpl.getActiveSize()

cacheSizeAfter = cacheImpl.getCacheSize()

} else {

assertNotNull(con)

}

assertEquals(activeSize, activeSizeAfter + 1)

/* 如果连接池中的缓存连接数大于最少缓存连接数,检查释放数据连接后是否

缓存连接数比之前减少了一个,反之缓存连接数是否保持为最少缓存连接数

*/

if (cacheSize >minConnections) {

assertEquals(cacheSize, cacheSizeAfter + 1)

} else {

assertEquals(cacheSize, minConnections)

}

}

/** 释放建立测试起始环境时的资源。

*/

protected void tearDown() {

cacheImpl = null

conProxy.destroy()

}

public DefaultConnectionProxyTest(String name) {

super(name)

}

/** 你可以简单的运行这个类从而对类中所包含的测试单元进行测试。

*/

public static void main(String args[]) {

junit.textui.TestRunner.run(DefaultConnectionProxyTest.class)

}

}

当单元测试完成后,我们可以用 Junit 提供的 TestSuite 对象对测试单元进行组织,你可以决定测试的顺序,然后运行你的测试。


欢迎分享,转载请注明来源:内存溢出

原文地址: http://www.outofmemory.cn/yw/12425991.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2023-05-25
下一篇 2023-05-25

发表评论

登录后才能评论

评论列表(0条)

保存