读书笔记 -- Junit 实战(3rd)Ch07 用 mock object 进行测试

发布时间 2023-11-14 12:27:46作者: bruce_he

8.1 mock object 简介

隔离测试:最大优点是能编写专门测试单一方法的测试代码,而不会受到被测方法调用某个对象所带来的副作用的影响。

mock object (mocks):非常适合测试与代码的其余部分隔离开的一部分代码。

 

mocks 与隔离测试的区别:mock 并不实现任何逻辑,只提供一些方法的空壳,让测试控制替代类的所有业务方法的行为。

 


 8.2 用 mock object 进行单元测试

// Account 类
@Data
@AllArgsConstructor
public class Account {
    // 账户 ID
    private String accountId;
    // 余额
    private long balance;

    public void debit(long amount) {
        this.balance -= amount;
    }

    public void credit(long amount) {
        this.balance += amount;
    }
}
// AccountManager 用户管理
public interface AccountManager {
    Account findAccountForUser(String userId);

    void updateAccount(Account account);
}
// AccountService 
public class AccountService {

    private AccountManager accountManager;

    // 设置一个 set(),将 将 accountManager 对象传过来。在 TestAccountService 中,将 mockAccountManager 传递过来
    // 或者,声明一个构造器
    public void setAccountManager(AccountManager accountManager) {
        this.accountManager = accountManager;
    }

    public void transfer(String senderId, String beneficiaryId, long amount) {
        Account sender = accountManager.findAccountForUser(senderId);
        Account beneficiary = accountManager.findAccountForUser(beneficiaryId);

        sender.debit(amount);
        beneficiary.credit(amount);

        this.accountManager.updateAccount(sender);
        this.accountManager.updateAccount(beneficiary);
    }
}

思路:

1. 完全实现的方法中的 TestAccountService 

public class TestAccountService {

    @Test
    public void testTransferOk() {
        Account senderAccount = new Account("1", 300);
        Account beneficiaryAccount = new Account("2", 100);
// 这里需要有个类来实现 接口 AccountManager,即 AccountManagerImpl,该对象可以 对多个 Account 进行管理,然后将该 对象 传递给 AccountService 进行使用进行转账
accountService.transfer(
"1", "2", 50); assertEquals(250, senderAccount.getBalance()); assertEquals(150, beneficiaryAccount.getBalance()); } }

实现:

1. Mock 一个 AccountManger,可以实现:1)可以存储多个 Account 对象,比较省力的结构是定义一个 Map 结构;2)实现 findAccountForUser() 方法

// MockAccountManager  类
public class MockAccountManager implements AccountManager {

    private Map<String, Account> accounts = new HashMap<>();

    // 将多个 Account 对象存储在 map 结构中,方便 findAccountForUser() 查找
    public void addAccount(String userId, Account account) {
        this.accounts.put(userId, account);
    }

    @Override
    // 该方法通过 userId 查找,返回一个 Account 对象。
    public Account findAccountForUser(String userId) {
        return this.accounts.get(userId);
    }

    @Override
    public void updateAccount(Account account) {
        // do nothing,因为测试 transfer 时不需要处理该逻辑
    }
}

2. 实现 TestAccountService 类

public class TestAccountService {

    @Test
    public void testTransferOk() {
        Account senderAccount = new Account("1", 300);
        Account beneficiaryAccount = new Account("2", 100);

        MockAccountManager mockAccountManager = new MockAccountManager();
        mockAccountManager.addAccount("1", senderAccount);
        mockAccountManager.addAccount("2", beneficiaryAccount);

        AccountService accountService = new AccountService();
        accountService.setAccountManager(mockAccountManager);

        accountService.transfer("1", "2", 50);

        assertEquals(250, senderAccount.getBalance());
        assertEquals(150, beneficiaryAccount.getBalance());
    }
}

 


 

8.4 模拟 HTTP 连接

// 原始的 WebClient
public class WebClient {
    public String getContent(URL url) {
        // 创建 StringBuffer 对象,存储可以递增的字符串
        StringBuffer content = new StringBuffer();

        try {
            // 使用给定的URL对象打开一个连接,并得到了一个HttpURLConnection对象。然后它进行了类型转换,将得到的连接对象转换为HttpURLConnection类型
            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
            // 设置一个网络连接可以读取输入流。
            // 在网络编程中,当你想要从网络连接读取数据时,你需要设置这个连接可以输入数据。
            // 在Java中,你可以使用 HttpURLConnection 或 URLConnection 类的 setDoInput(boolean doInput) 方法来
            //          设置这个连接可以输入数据。doInput 参数为 true 表示允许从连接读取数据,为 false 则表示不允许。
            // 这行代码通常在建立网络连接之前使用,以确保你可以从该连接读取数据。
            connection.setDoInput(true);
            // 通过HttpURLConnection对象调用getInputStream()方法,得到一个输入流is,这个输入流用于读取从URL获取的数据
            InputStream is = connection.getInputStream();

            int count;
            // 循环持续读取输入流中的数据,直到没有数据可读(当is.read()返回-1时)。
            // 每次读取一个字符,使用Character.toChars(count)将字符代码转换为字符,并追加到StringBuffer中
            while (-1 != (count = is.read())) {
                // Character.toChars(count) 将一个Unicode码转换为对应的字符。例如,如果 count 是65,那么这个方法会返回字符 'A'。
                // 再通过 new String(char value[]) 转换为字符串
                content.append(new String(Character.toChars(count)));
            }
        } catch (IOException e) {
            return null;
        }

        return content.toString();
    }
}

思路:模拟一个 URL,其中 url.openConnection() 返回一个 mock HttpURLConnection。通过 MockHttpURLConnection 决定 getInputStream() 返回什么。

           但是,URL 是一个 final 类,没有接口可用。

解决方案:创建一个 ConnectionFactory 接口,类实现 ConnectionFactory 接口的作用是从一个连接返回一个 InputStream,无论连接时什么(HTTP、TCP/IP 等)。该重构技术被称为 类工厂重构。

// 使用类工厂重构的 WebClient2
public class WebClient2 {

    public String getContent(ConnectionFactory connectionFactory) {
        String workingContent;
        StringBuffer content = new StringBuffer();

        try (InputStream is = connectionFactory.getData()) {
            int count;

            while (-1 != (count = is.read())) {
                content.append(new String(Character.toChars(count)));
            }
            workingContent = content.toString();
        } catch (Exception e) {
            workingContent = null;
        }
        return workingContent;
    }
}

// 对应的 ConnectionFactory 接口
public interface ConnectionFactory {
    InputStream getData() throws Exception;
}

测试新的 WebClient2:

思路:需要 Mock 一个 ConnectionFactory,可以将想要产生的结果通过 setter() 传给 MockConnectionFactory 类

// MockConnectionFactory 
public class MockConnectionFactory implements ConnectionFactory {
    private InputStream inputStream;

    public void setData(InputStream stream) {
        this.inputStream = stream;
    }

    @Override
    public InputStream getData() throws Exception {
        return inputStream;
    }
}
// TestWebClient 
public class TestWebClient {

    @Test
    public void testGetContentOk() {

        MockConnectionFactory mockConnectionFactory = new MockConnectionFactory();
        mockConnectionFactory.setData(new ByteArrayInputStream("It works".getBytes()));

        WebClient2 client = new WebClient2();
        String workingContent = client.getContent(mockConnectionFactory);

        assertEquals("It works", workingContent);
    }
}

 


 

8.6 mock 框架

8.6.1 EasyMock

// pom.xml 添加依赖
<!-- EasyMock 依赖项,仅可以 mock 接口 -->
<dependency> <groupId>org.easymock</groupId> <artifactId>easymock</artifactId> <version>5.1.0</version> </dependency>
<!-- EasyMock 扩展项,可为类和接口生成 mock object --> <dependency> <groupId>org.easymock</groupId> <artifactId>easymockclassextension</artifactId> <version>3.2</version> </dependency>

EasyMock 的几个重点:

  • EasyMock 框架只能 mock 对象;
  • 使用 EasyMock 有两种声明预期的方式:1)返回为 void 时,在模拟对象上调用;2)返回任何类型的对象时,使用 EasyMock API 的 expect 和 andReturn 方法;
  • 完成对预期的定义时,调用 reply(),该方法将 mock 从记录预期被调用的方法的地方传递到测试的地方;
  • @AfterEach 使用任何模拟对象调用 verify() 验证是否触发了声明的方法调用预期;
public class TestAccountServiceEasyMock {
    // 这里声明 AccountManager,核心原因是 EasyMock框架只能 mock 接口对象
    private AccountManager mockAccountManager;

    @BeforeEach
    public void setUp() {
        // step1: 调用 createMock() 创建所需类的一个 mock
        mockAccountManager = createMock("mockAccountManager", AccountManager.class);
    }

    @Test
    public void testTransferOk() {

        // 创建两个 Account 对象
        Account senderAccount = new Account("1", 300);
        Account beneficiaryAccount = new Account("2", 100);

        // step2: 声明 预期 的方式一:返回为 void,直接在对象上调用
        mockAccountManager.updateAccount(senderAccount);
        mockAccountManager.updateAccount(beneficiaryAccount);

        // 声明预期的方式二:返回任何类型的对象时,使用 EasyMock API 的 expect 和 andReturn 方法
        expect(mockAccountManager.findAccountForUser("1")).andReturn(senderAccount);
        expect(mockAccountManager.findAccountForUser("2")).andReturn(beneficiaryAccount);

        // step3: 完成对预期的定义时,调用 reply()
        replay(mockAccountManager);

        // 调用 transfer
        AccountService accountService = new AccountService();
        accountService.setAccountManager(mockAccountManager);
        accountService.transfer("1", "2", 50);

        // 验证
        assertEquals(250, senderAccount.getBalance());
        assertEquals(150, beneficiaryAccount.getBalance());
    }

    @AfterEach
    public void tearDown() {
        // @AfterEach 使用任何模拟对象调用 verify() 验证是否触发了声明的方法调用预期
        verify(mockAccountManager);
    }
}