创建单元测试、集成测试

阅读更多

在面向对象语言中,一个单元通常是一个类或一个方法。但是在现实中,大多数单元不是单独工作的。它们

通常需要和其他单元合作以实现它们的任务。

 

当测试的单元依赖了其他的单元时,有一个通用技术可用来模拟依赖单元,它用的是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 accounts;

    public InMemoryAccountDao() {
        accounts = Collections.synchronizedMap(new HashMap());
    }

    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库的依赖


  org.mockito
  mockito-core
  1.9.5

 

下面是测试代码:

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);
    }
}

 

 

你可能感兴趣的:(junit,Mockito)