欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

创建单元测试、集成测试

程序员文章站 2022-04-16 14:31:39
...

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

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

 

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

 

 

相关标签: junit Mockito