In object-oriented languages, a unit is usually a class or a method. But in reality, most units do not work individually. they
It is often necessary to cooperate with other units to achieve their tasks.
When the unit under test depends on other units, there is a general technique that can be used to simulate the dependent unit, it uses stub and mock objects,
Both of these can reduce the complexity of unit testing due to dependencies.
A stub object contains the minimum number of methods to be used in a test. These methods are usually done in a predictable way, i.e.
Hardcoded data. In Java, there are several libraries that help create Mock objects, including EasyMock and jMock.
The main difference between stub and mock objects is: stub is used for state verification, mock is used for behavior verification
Integration tests are used to test several units as a whole. Test that these units interact with each other correctly, these units should all have
It's unit tested, so integration tests are usually done after unit tests.
Finally, note that applications developed using dependency injection, which isolates the interface from the implementation, are easier to test because these principles and patterns enable
Reduce the coupling between different units in the application.
1. Create unit tests for isolated classes
The core functionality of the banking system should be designed around customer account numbers. First, you create the following domain class Account, overriding the equals method:
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;
}
}
Next is the DAO interface for persisting account objects:
public interface AccountDao {
public void createAccount(Account account);
public void updateAccount(Account account);
public void removeAccount(Account account);
public Account findAccount(String accountNo);
}
To introduce the concept of unit testing, use a map to store account objects to implement the above interface:
where AccountNotFoundException and DuplicateAccountException are both subclasses of RuntimeException, you should
know how to create them.
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;
}
}
Obviously, this simple DAO implementation does not support transactions. However, to make it thread-safe, you can use a synchronized map to
Wraps the original map so that it is accessed serially.
Now, let's write a unit test for this DAO implementation class using JUnit 4, since this class does not directly depend on other classes, it will be easier to test.
To ensure that this class behaves properly for exceptions as well as normal cases, you should also create exception test cases for it.
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);
}
}
2. Use Stubs and Mocks objects to create unit tests for dependent classes
Testing classes that have dependencies on other classes or services is a little harder.
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);
}
The implementation of this interface needs to rely on an AccountDao object of the persistence layer to persist the account object. one of them
InsufficientBalanceException is also a subclass of 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();
}
}
Stubs can be used to reduce the complexity of unit testing due to dependencies. A stub must implement the same interface as the target object,
So that it can replace the target object.
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);
}
}
However, it takes too much code to implement stubs yourself, and a more efficient technique is mock objects. The Mockito library can dynamically create mock objects.
Add a dependency on the Mockito library to Maven's pom.xml
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>1.9.5</version>
</dependency>
Here is the test code:
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);
}
}