创建单元测试、集成测试
在面向对象语言中,一个单元通常是一个类或一个方法。但是在现实中,大多数单元不是单独工作的。它们
通常需要和其他单元合作以实现它们的任务。
当测试的单元依赖了其他的单元时,有一个通用技术可用来模拟依赖单元,它用的是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);
}
}