Create unit tests, integration tests

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

 

 

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=326265414&siteId=291194637