在面向对象语言中,一个单元通常是一个类或一个方法。但是在现实中,大多数单元不是单独工作的。它们
通常需要和其他单元合作以实现它们的任务。
当测试的单元依赖了其他的单元时,有一个通用技术可用来模拟依赖单元,它用的是stub和mock对象,
这两者能够降低单元测试由于依赖而导致的复杂性。
stub对象中包含了某个测试中要用到的最少数量的方法。这些方法通常都是以一种预知的方式完成的,也就是
硬编码的数据。在Java中,有几个库可帮助创建Mock对象,包括EasyMock和jMock。
stub和mock对象间的主要区别在于:stub用于state verification,mock用于behavior verification
集成测试用于把若干个单元作为一个整体来测。测试这些单元相互间的交互是否正确,这些单元应该都已经
经过了单元测试,因此集成测试通常是在单元测试后进行的。
最后,注意,使用了将接口与实现隔离、依赖注入开发的应用更易测试,那是因为这些原则和模式能够
降低应用中的不同单元间的耦合性。
一、为隔离的类创建单元测试
银行系统的核心功能应当围绕客户账号来设计。首先,你创建下面的一个领域类Account,重写了equals方法:
public class Account {
private String accountNo;
private double balance;
// Constructors, Getters and Setters
...
public boolean equals(Object obj) {
if (!(obj instanceof Account)) {
return false;
}
Account account = (Account) obj;
return account.accountNo.equals(accountNo) && account.balance == balance;
}
}
接下来是用于持久化账号对象的DAO接口:
public interface AccountDao {
public void createAccount(Account account);
public void updateAccount(Account account);
public void removeAccount(Account account);
public Account findAccount(String accountNo);
}
为了介绍单元测试概念,用一个map来存储账号对象来实现上面的接口:
其中AccountNotFoundException和DuplicateAccountException都是RuntimeException的子类,你应当
知道怎么创建它们。
public class InMemoryAccountDao implements AccountDao {
private Map<String, Account> accounts;
public InMemoryAccountDao() {
accounts = Collections.synchronizedMap(new HashMap<String, Account>());
}
public boolean accountExists(String accountNo) {
return accounts.containsKey(accountNo);
}
public void createAccount(Account account) {
if (accountExists(account.getAccountNo())) {
throw new DuplicateAccountException();
}
accounts.put(account.getAccountNo(), account);
}
public void updateAccount(Account account) {
if (!accountExists(account.getAccountNo())) {
throw new AccountNotFoundException();
}
accounts.put(account.getAccountNo(), account);
}
public void removeAccount(Account account) {
if (!accountExists(account.getAccountNo())) {
throw new AccountNotFoundException();
}
accounts.remove(account.getAccountNo());
}
public Account findAccount(String accountNo) {
Account account = accounts.get(accountNo);
if (account == null) {
throw new AccountNotFoundException();
}
return account;
}
}
很显然,这个简单的DAO实现不支持事务。不过,为了使得它是线程安全的,你可以用一个同步的map来
包装原始的map,这样就是串行访问了。
现在,让我们用JUnit 4为此DAO实现类写个单元测试,因为此类不直接依赖其他类,那测起来就容易了。
为了确保此类对于异常情况以及正常情况中适当地运行,你还应当为其创建异常测试用例。
public class InMemoryAccountDaoTests {
private static final String EXISTING_ACCOUNT_NO = "1234";
private static final String NEW_ACCOUNT_NO = "5678";
private Account existingAccount;
private Account newAccount;
private InMemoryAccountDao accountDao;
@Before
public void init() {
existingAccount = new Account(EXISTING_ACCOUNT_NO, 100);
newAccount = new Account(NEW_ACCOUNT_NO, 200);
accountDao = new InMemoryAccountDao();
accountDao.createAccount(existingAccount);
}
@Test
public void accountExists() {
assertTrue(accountDao.accountExists(EXISTING_ACCOUNT_NO));
assertFalse(accountDao.accountExists(NEW_ACCOUNT_NO));
}
@Test
public void createNewAccount() {
accountDao.createAccount(newAccount);
assertEquals(accountDao.findAccount(NEW_ACCOUNT_NO), newAccount);
}
@Test(expected = DuplicateAccountException.class)
public void createDuplicateAccount() {
accountDao.createAccount(existingAccount);
}
@Test
public void updateExistedAccount() {
existingAccount.setBalance(150);
accountDao.updateAccount(existingAccount);
assertEquals(accountDao.findAccount(EXISTING_ACCOUNT_NO), existingAccount);
}
@Test(expected = AccountNotFoundException.class)
public void updateNotExistedAccount() {
accountDao.updateAccount(newAccount);
}
@Test
public void removeExistedAccount() {
accountDao.removeAccount(existingAccount);
assertFalse(accountDao.accountExists(EXISTING_ACCOUNT_NO));
}
@Test(expected = AccountNotFoundException.class)
public void removeNotExistedAccount() {
accountDao.removeAccount(newAccount);
}
@Test
public void findExistedAccount() {
Account account = accountDao.findAccount(EXISTING_ACCOUNT_NO);
assertEquals(account, existingAccount);
}
@Test(expected = AccountNotFoundException.class)
public void findNotExistedAccount() {
accountDao.findAccount(NEW_ACCOUNT_NO);
}
}
二、利用Stubs以及Mocks对象为依赖类创建单元测试
测试那些对其他类或服务有依赖的类就有些难了。
public interface AccountService {
public void createAccount(String accountNo);
public void removeAccount(String accountNo);
public void deposit(String accountNo, double amount);
public void withdraw(String accountNo, double amount);
public double getBalance(String accountNo);
}
该接口的实现需要依赖持久层的一个AccountDao对象来持久化账号对象。其中的
InsufficientBalanceException同样是RuntimeException的子类。
public class AccountServiceImpl implements AccountService {
private AccountDao accountDao;
public AccountServiceImpl(AccountDao accountDao) {
this.accountDao = accountDao;
}
public void createAccount(String accountNo) {
accountDao.createAccount(new Account(accountNo, 0));
}
public void removeAccount(String accountNo) {
Account account = accountDao.findAccount(accountNo);
accountDao.removeAccount(account);
}
public void deposit(String accountNo, double amount) {
Account account = accountDao.findAccount(accountNo);
account.setBalance(account.getBalance() + amount);
accountDao.updateAccount(account);
}
public void withdraw(String accountNo, double amount) {
Account account = accountDao.findAccount(accountNo);
if (account.getBalance() < amount) {
throw new InsufficientBalanceException();
}
account.setBalance(account.getBalance() - amount);
accountDao.updateAccount(account);
}
public double getBalance(String accountNo) {
return accountDao.findAccount(accountNo).getBalance();
}
}
stub就可用来降低单元测试中由于依赖而导致的复杂性。一个stub必须实现了target对象一样的接口,
这样它才能代替target对象。
public class AccountServiceImplStubTests {
private static final String TEST_ACCOUNT_NO = "1234";
private AccountDaoStub accountDaoStub;
private AccountService accountService;
private class AccountDaoStub implements AccountDao {
private String accountNo;
private double balance;
public void createAccount(Account account) {}
public void removeAccount(Account account) {}
public Account findAccount(String accountNo) {
return new Account(this.accountNo, this.balance);
}
public void updateAccount(Account account) {
this.accountNo = account.getAccountNo();
this.balance = account.getBalance();
}
}
@Before
public void init() {
accountDaoStub = new AccountDaoStub();
accountDaoStub.accountNo = TEST_ACCOUNT_NO;
accountDaoStub.balance = 100;
accountService = new AccountServiceImpl(accountDaoStub);
}
@Test
public void deposit() {
accountService.deposit(TEST_ACCOUNT_NO, 50);
assertEquals(accountDaoStub.accountNo, TEST_ACCOUNT_NO);
assertEquals(accountDaoStub.balance, 150, 0);
}
@Test
public void withdrawWithSufficientBalance() {
accountService.withdraw(TEST_ACCOUNT_NO, 50);
assertEquals(accountDaoStub.accountNo, TEST_ACCOUNT_NO);
assertEquals(accountDaoStub.balance, 50, 0);
}
@Test(expected = InsufficientBalanceException.class)
public void withdrawWithInsufficientBalance() {
accountService.withdraw(TEST_ACCOUNT_NO, 150);
}
}
不过自己实现stubs要写太多代码,更高效的技术是mock对象。Mockito库能够动态创建mock对象。
在Maven的pom.xml中加入对Mockito库的依赖
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>1.9.5</version>
</dependency>
下面是测试代码:
import org.junit.Before;
import org.junit.Test;
import static org.mockito.Mockito.*;
public class AccountServiceImplMockTests {
private static final String TEST_ACCOUNT_NO = "1234";
private AccountDao accountDao;
private AccountService accountService;
@Before
public void init() {
accountDao = mock(AccountDao.class);
accountService = new AccountServiceImpl(accountDao);
}
@Test
public void deposit() {
// Setup
Account account = new Account(TEST_ACCOUNT_NO, 100);
when(accountDao.findAccount(TEST_ACCOUNT_NO)).thenReturn(account);
// Execute
accountService.deposit(TEST_ACCOUNT_NO, 50);
// Verify
verify(accountDao, times(1)).findAccount(any(String.class));
verify(accountDao, times(1)).updateAccount(account);
}
@Test
public void withdrawWithSufficientBalance() {
// Setup
Account account = new Account(TEST_ACCOUNT_NO, 100);
when(accountDao.findAccount(TEST_ACCOUNT_NO)).thenReturn(account);
// Execute
accountService.withdraw(TEST_ACCOUNT_NO, 50);
// Verify
verify(accountDao, times(1)).findAccount(any(String.class));
verify(accountDao, times(1)).updateAccount(account);
}
@Test(expected = InsufficientBalanceException.class)
public void testWithdrawWithInsufficientBalance() {
// Setup
Account account = new Account(TEST_ACCOUNT_NO, 100);
when(accountDao.findAccount(TEST_ACCOUNT_NO)).thenReturn(account);
// Execute
accountService.withdraw(TEST_ACCOUNT_NO, 150);
}
}
相关推荐
在Spring框架中,单元测试和集成测试是软件开发过程中不可或缺的部分。它们确保代码的质量和功能的正确性。本文将深入探讨Spring3中的单元测试和集成测试,并提供相关的实践指导。 ### 单元测试 单元测试是指针对...
Visual Studio作为一款强大的集成开发环境,不仅支持代码编写和调试,还内置了完善的单元测试工具。本教程将详细介绍如何在VS中利用其自身的工具进行单元测试,包括测试项目的创建、不同类型的测试以及如何运行和...
Flex持续集成之单元测试是开发过程中一个至关重要的环节,它主要关注的是Flex应用程序的模块化测试,确保每个独立的代码单元都能正常工作。通过单元测试,开发者可以预先发现和修复问题,提高代码质量和稳定性,同时...
在创建单元测试时,我们需要遵循几个最佳实践。首先是保证测试的隔离性,这意味着测试不应依赖于外部环境或其它非确定性因素,如数据库状态或网络连接。为了实现这一点,我们可以使用mocking库(如Moq或NSubstitute...
经验的开发者,理解并掌握单元测试和集成测试都是至关重要的。在Python中,unittest和pytest是两种常用的单元测试框架,它们各自拥有独特的特性和优势。unittest作为Python内置的框架,适用于基本的测试需求,而...
它可以与 JUnit 集成,提供详细的单元测试覆盖率报告。Clover 提供了多种报告格式,包括 HTML、XML 和 CSV。 使用 Clover 分析 JUnit 测试 要使用 Clover 分析 JUnit 测试,首先需要下载 Clover 的程序包,包括 ...
雨田单元测试系统2.0是拥有独立知识产权的国产测试软件,能够对c文件进行单元测试和集成测试。它以被测单元为纽带,将首次测试以及后续的回归测试有机的关联起来,使测试人员在充分利用历史用例的基础上,集中精力对...
4. 集成测试和持续集成:如何将单元测试与版本控制系统、构建服务器和持续集成流程相结合。 5. 回归测试和调试:如何利用Visual Studio进行回归测试,查找并修复失败的测试,以及测试中的调试技巧。 6. 性能测试和...
在现代软件开发中,数据库单元测试是确保数据库应用程序质量和功能正确性的重要环节。随着软件功能的日益复杂,数据库处理部分变得越来越复杂,涉及到大量的存储过程和触发器。为了提高测试效率和质量,必须找到能够...
【标题】"最简单的含单元测试的spring boot+activiti集成demo" 描述了一种将流行的Spring Boot框架与流程管理工具Activiti结合使用的实践案例。这个集成演示项目旨在帮助开发者快速理解和实现基于Spring Boot的...
在软件开发过程中,单元测试是一种重要的质量保证手段,它针对代码的最小可测试单元——函数、方法或类,进行独立验证。本示例主要聚焦于C++编程语言中的单元测试,我们将探讨如何进行白盒测试的基本实现方法和结构...
在软件开发领域,尤其是汽车电子软件的开发过程中,单元测试是一项至关重要的质量保证步骤。单元测试是对软件中的最小可测试单元进行检查,通常是函数、方法或类,以确保它们按照预期工作。它有助于早期发现和修复...
CUnit 是一个流行的开源测试框架,专为C语言设计,用于进行单元测试和简单的集成测试。这个工具使得C程序员能够方便地编写和组织他们的测试用例,确保代码的质量和可靠性。下面将详细介绍CUnit的特点、使用方法以及...