首页 > 其他分享 >读书笔记 -- Junit 实战(3rd)Ch07 用 mock object 进行测试

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

时间:2023-11-14 12:34:00浏览次数:30  
标签:Account String 3rd 读书笔记 -- void mockAccountManager new public

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

 

标签:Account,String,3rd,读书笔记,--,void,mockAccountManager,new,public
From: https://www.cnblogs.com/bruce-he/p/17825979.html

相关文章

  • js:可选链运算符(?.)和空值合并运算符(??)
    文档:可选链运算符(?.)https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/Optional_chaining空值合并运算符(??)https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing目录1、可选链运算符(?.)2、空值合并运算......
  • 以太网通信控制板-A3-控制板作为TCP服务器和电脑TCP客户端通信(连接电脑网线方式, TCP
    <p><iframename="ifd"src="https://mnifdv.cn/resource/cnblogs/CH579_DTU_PBX/index1.html"frameborder="0"scrolling="auto"width="100%"height="1500"></iframe></p> 说明这节......
  • 基于React使用swiperjs实现竖向滚动自动轮播
    很多文章,都只提供了js部分,包括官方的文档也只有js部分,如果css设置不正确,会导致轮播图不自动播放。使用的swiper版本:v11.0.3文档https://swiperjs.com/get-startedhttps://swiperjs.com/react实现效果使用vite创建react应用pnpmcreatevitereact-app--templatereact完整依赖pac......
  • 使用 powershell 安装 openssh
    1. 若要使用PowerShell安装OpenSSH,请先以管理员身份运行PowerShell(win+x快捷键进行打开)。为了确保OpenSSH可用,请运行以下cmdlet:Get-WindowsCapability-Online|Where-ObjectName-like'OpenSSH*'  2. 如果两者均尚未安装,则此操作应返回以下输出:Name:Op......
  • CoreFX中Dictionary<TKey, TValue>的源码解读
    无论是实际的项目中,还是在我们学习的过程中,都会重点的应用到Dictionary<TKey,TValue>这个存储类型。每次对Dictionary<TKey,TValue>的添加都包含一个值和与其关联的键,使用键检索值的速度非常快,接近O(1),因为 Dictionary<TKey,TValue>类是作为哈希表实现的。首先我们来......
  • PostCSS通过px2rem插件和lib-flexible将px单位转换为rem(root em)单位实现大屏适配
    目录文档postcss中使用postcss-plugin-px2rem安装postcss-plugin-px2rem示例默认配置webpack中使用postcss-plugin-px2rem项目结构安装依赖文件内容大屏适配参考文章文档类似的插件postcss-plugin-px2remhttps://www.npmjs.com/package/postcss-plugin-px2remhttps://github.com/......
  • Bean常用的属性
    Bean常用的属性介绍<beanname="xxx"class="指定的bean类"scope="singleton"></bean>1、Id属性:javabean在BeanFactory中的唯一标识,代码中通过BeanFactory获取JavaBean实例时需以此作为索引名称2、Name属性:同id大致相同,如果给bean增加别名,可以通过name属性指定一个或多个i......
  • 【BUG解决】服务器没报警但是应用接口崩了....
    最近遇到一个突发问题:服务器没报警但是应用接口崩了…为其他业务系统提供一个接口,平时好好的,突然就嚷嚷反馈说访问不了了,吓得我赶紧跳起来!正常情况下在系统崩溃前,我会收到很多系统报警,但是这次它悄无声息的出问题,还是挺恐怖的然后我立马看了下服务器的情况,服务器没有报警,也可以pin......
  • 1821_ChibiOS中的事件机制
    GreyZhang/g_ChibiOS:IfoundanewRTOScalledChibiOSanditseemsinteresting!(github.com)1.这里开篇就讲了ChibiOS中的事件机制非常强大,算是OS的一个子系统功能。提供了多事件等待、事件与线程多对多、异步广播同步检查等功能。2.事件机制涉及到的三个对类别分别是:事件......
  • 1824_ChibiOS的OS库
    GreyZhang/g_ChibiOS:IfoundanewRTOScalledChibiOSanditseemsinteresting!(github.com)1.对于ChibiOS来说,OSLIB是一个可选的部分。2.这部分有些功能之前是在内核中的,但是由于内核是两种,RT以及NIL,这里面的代码是有一些会重复的。这样,重复的一部分功能也被拆分了出来。......