成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

深入理解單元測(cè)試:技巧與優(yōu)秀實(shí)踐

開(kāi)發(fā) 前端
對(duì)于一些功能單一、核心邏輯、同時(shí)變化不頻繁的公開(kāi)函數(shù)才有必要做單元測(cè)試。對(duì)于業(yè)務(wù)復(fù)雜、鏈路繁瑣但也是核心流程的功能通常建議做 e2e 測(cè)試,這樣可以保證最終測(cè)試結(jié)果的一致性。


之前分享過(guò)如何快速上手開(kāi)源項(xiàng)目以及如何在開(kāi)源項(xiàng)目里做集成測(cè)試,但還沒(méi)有講過(guò)具體的實(shí)操。

今天來(lái)詳細(xì)講講如何寫(xiě)單元測(cè)試。

什么情況下需要單元測(cè)試

這個(gè)大家應(yīng)該是有共識(shí)的,對(duì)于一些功能單一、核心邏輯、同時(shí)變化不頻繁的公開(kāi)函數(shù)才有必要做單元測(cè)試。

對(duì)于業(yè)務(wù)復(fù)雜、鏈路繁瑣但也是核心流程的功能通常建議做 e2e 測(cè)試,這樣可以保證最終測(cè)試結(jié)果的一致性。

具體案例

我們都知道單測(cè)的主要目的是模擬執(zhí)行你寫(xiě)過(guò)的每一行代碼,目的就是要覆蓋到主要分支,做到自己的每一行代碼都心中有數(shù)。

下面以 Apache HertzBeat 的一些單測(cè)為例,講解如何編寫(xiě)一個(gè)單元測(cè)試。

先以一個(gè)最簡(jiǎn)單的 org.apache.hertzbeat.collector.collect.udp.UdpCollectImpl#preCheck 函數(shù)測(cè)試為例。這里的 preCheck 函數(shù)就是簡(jiǎn)單的檢測(cè)做參數(shù)校驗(yàn)。測(cè)試時(shí)只要我們手動(dòng)將 metrics 設(shè)置為 null 就可以進(jìn)入這個(gè) if 條件。

@ExtendWith(MockitoExtension.class)
class UdpCollectImplTest {

    @InjectMocks
    private UdpCollectImpl udpCollect;

    @Test
    void testPreCheck() {
        List<String> aliasField = new ArrayList<>();
        aliasField.add("responseTime");
        Metrics metrics = new Metrics();
        metrics.setAliasFields(aliasField);
        assertThrows(IllegalArgumentException.class, () -> udpCollect.preCheck(metrics));
    }
}

來(lái)看具體的單測(cè)代碼,我們一行行的來(lái)看:

@ExtendWith(MockitoExtension.class) 是 Junit5 提供的一個(gè)注解,里面?zhèn)魅氲?nbsp;MockitoExtension.class 是我們單測(cè) mock 常用的框架。

簡(jiǎn)單來(lái)說(shuō)就是告訴 Junit5 ,當(dāng)前的測(cè)試類會(huì)使用 mockito 作為擴(kuò)展運(yùn)行,從而可以 mock 我們運(yùn)行時(shí)的一些對(duì)象。

@InjectMocks  
private UdpCollectImpl udpCollect;

@InjectMocks 也是 mockito 這個(gè)庫(kù)提供的注解,通常用于聲明需要測(cè)試的類。

@InjectMocks  
private AbstractCollect udpCollect;

需要注意的是這個(gè)注解必須是一個(gè)具體的類,不可以是一個(gè)抽象類或者是接口。

其實(shí)當(dāng)我們了解了他的原理就能知道具體的原因:

當(dāng)我們 debug 運(yùn)行時(shí)會(huì)發(fā)現(xiàn) udpCollect 對(duì)象是有值的,而如果我們?nèi)サ暨@個(gè)注解 @InjectMocks 再運(yùn)行就會(huì)拋空指針異常。

因?yàn)椴](méi)有初始化 udpCollect

而使用 @InjectMocks注解后,mockito 框架會(huì)自動(dòng)給 udpCollect 注入一個(gè)代理對(duì)象;而如果是一個(gè)接口或者是抽象類,mockito 框架是無(wú)法知道創(chuàng)建具體哪個(gè)對(duì)象。

當(dāng)然在這個(gè)簡(jiǎn)單場(chǎng)景下,我們直接 udpCollect = new UdpCollectImpl() 進(jìn)行測(cè)試也是可以的。

配合 jacoco 輸出單測(cè)覆蓋率

在 IDEA 中我們可以以 Coverage 的方式運(yùn)行,IDEA 就將我們的單測(cè)覆蓋情況顯示在源代碼中,綠色的部分就代表在實(shí)際在運(yùn)行時(shí)執(zhí)行到的地方。

我們也可以在 maven 項(xiàng)目中集成 jacoco,只需要添加一個(gè)根目錄的 pom.xml 中添加一個(gè) plugin 就可以了。

<plugin>  
    <groupId>org.jacoco</groupId>  
    <artifactId>jacoco-maven-plugin</artifactId>  
    <version>${jacoco-maven-plugin.version}</version>  
    <executions>  
        <execution>  
            <goals>  
                <goal>prepare-agent</goal>  
            </goals>  
        </execution>  
        <execution>  
            <id>report</id>  
            <phase>test</phase>  
            <goals>  
                <goal>report</goal>  
            </goals>  
        </execution>  
    </executions>  
</plugin>

之后運(yùn)行 mvn test 就會(huì)在 target 目錄下生成測(cè)試報(bào)告了。

我們還可以在 GitHub 的 CI 中集成 Codecov,他會(huì)直接讀取 jacoco 的測(cè)試數(shù)據(jù),并且在 PR 的評(píng)論區(qū)加上測(cè)試報(bào)告。

需要從 Codecov 里將你項(xiàng)目的 token 添加到 repo 的 環(huán)境變量中即可。

具體可以參考這個(gè) PR:https://github.com/apache/hertzbeat/pull/1985

復(fù)雜一點(diǎn)的單測(cè)

剛才展示的是一個(gè)非常簡(jiǎn)單的場(chǎng)景,下面來(lái)看看稍微復(fù)雜的。

我們以這個(gè)單測(cè)為例:org.apache.hertzbeat.collector.collect.redis.RedisClusterCollectImplTest

@ExtendWith(MockitoExtension.class)
public class RedisClusterCollectImplTest {
    
    @InjectMocks
    private RedisCommonCollectImpl redisClusterCollect;


    @Mock
    private StatefulRedisClusterConnection<String, String> connection;

    @Mock
    private RedisAdvancedClusterCommands<String, String> cmd;

    @Mock
    private RedisClusterClient client;
}

這個(gè)單測(cè)在剛才的基礎(chǔ)上多了一個(gè) @Mock 的注解。

這是因?yàn)槲覀冃枰獪y(cè)試的 RedisCommonCollectImpl 類中需要依賴 StatefulRedisClusterConnection/RedisAdvancedClusterCommands/RedisClusterClient 這幾個(gè)類所提供的服務(wù)。

單測(cè)的時(shí)候需要使用 mockito 創(chuàng)建一個(gè)他們的對(duì)象,并且注入到需要被測(cè)試的 RedisCommonCollectImpl類中。

不然我們就需要準(zhǔn)備單測(cè)所需要的資源,比如可以使用的 Redis、MySQL 等。

模擬行為

只是注入進(jìn)去還不夠,我們還需要模擬它的行為:

  • 比如調(diào)用某個(gè)函數(shù)可以模擬返回?cái)?shù)據(jù)
  • 模擬函數(shù)調(diào)用拋出異常
  • 模擬函數(shù)調(diào)用耗時(shí)

這里以最常見(jiàn)的模擬函數(shù)返回為例:

String clusterNodes = connection.sync().clusterInfo();

在源碼里看到會(huì)使用 connection 的 clusterInfo() 函數(shù)返回集群信息。

String clusterKnownNodes = "2";
        String clusterInfoTemp = """
                cluster_slots_fail:0
                cluster_known_nodes:%s
                """;
        String clusterInfo = String.format(clusterInfoTemp, clusterKnownNodes);
        Mockito.when(cmd.clusterInfo()).thenReturn(clusterInfo);

此時(shí)我們就可以使用 Mockito.when().thenReturn() 來(lái)模擬這個(gè)函數(shù)的返回?cái)?shù)據(jù)。

而其中的 cmd 自然也是需要模擬返回的:

Mockito.mockStatic(RedisClusterClient.class).when(()->RedisClusterClient.create(Mockito.any(ClientResources.class),
                Mockito.any(RedisURI.class))).thenReturn(client);
        Mockito.when(client.connect()).thenReturn(connection);
        
        Mockito.when(connection.sync()).thenReturn(cmd);
        Mockito.when(cmd.info(metrics.getName())).thenReturn(info);
        Mockito.when(cmd.clusterInfo()).thenReturn(clusterInfo);

cmd 是通過(guò) Mockito.when(connection.sync()).thenReturn(cmd);返回的,而 connection 又是從 client.connect() 返回的。

最終就像是套娃一樣,client 在源碼中是通過(guò)一個(gè)靜態(tài)函數(shù)創(chuàng)建的。

模擬靜態(tài)函數(shù)

我依稀記得在我剛接觸 mockito 的 16~17 年那段時(shí)間還不支持模擬調(diào)用靜態(tài)函數(shù),不過(guò)如今已經(jīng)支持了:

@Mock  
private RedisClusterClient client;


Mockito.mockStatic(RedisClusterClient.class).when(()->RedisClusterClient.create(Mockito.any(ClientResources.class),  
        Mockito.any(RedisURI.class))).thenReturn(client);

這樣就可以模擬靜態(tài)函數(shù)的返回值了,但前提是返回的 client 需要使用 @Mock 注解。

模擬構(gòu)造函數(shù)

有時(shí)候我們也需要模擬構(gòu)造函數(shù),從而可以模擬后續(xù)這個(gè)對(duì)象的行為。

MockedConstruction<FTPClient> mocked = Mockito.mockConstruction(FTPClient.class,
                (ftpClient, context) -> {
                    Mockito.doNothing().when(ftpClient).connect(ftpProtocol.getHost(),
                            Integer.parseInt(ftpProtocol.getPort()));

                    Mockito.doAnswer(invocationOnMock -> true).when(ftpClient)
                            .login(ftpProtocol.getUsername(), ftpProtocol.getPassword());
                    Mockito.when(ftpClient.changeWorkingDirectory(ftpProtocol.getDirection())).thenReturn(isActive);
                    Mockito.doNothing().when(ftpClient).disconnect();
                });

可以使用 Mockito.mockConstruction 來(lái)進(jìn)行模擬,該對(duì)象的一些行為就直接寫(xiě)在這個(gè)模擬函數(shù)內(nèi)。

需要注意的是返回的 mocked 對(duì)象需要記得關(guān)閉。

不需要 Mock

當(dāng)然也不是所有的場(chǎng)景都需要 mock。

比如剛才第一個(gè)場(chǎng)景,沒(méi)有依賴任何外部服務(wù)時(shí)就不需要 mock。

類似于這個(gè) PR 里的測(cè)試,只是依賴一個(gè)基礎(chǔ)的內(nèi)存緩存組件,就沒(méi)必要 mock,但如果依賴的是 Redis 緩存組件還是需要 mock 的。https://github.com/apache/hertzbeat/pull/2021

修改源碼

如果有些測(cè)試場(chǎng)景下需要獲取內(nèi)部變量方便后續(xù)的測(cè)試,但是該測(cè)試類也沒(méi)有提供獲取變量的函數(shù),我們就只有修改源碼來(lái)配合測(cè)試了。

比如這個(gè) PR:

當(dāng)然如果只是給測(cè)試環(huán)境下使用的函數(shù)或變量,我們可以加上 @VisibleForTesting注解標(biāo)明一下,這個(gè)注解沒(méi)有其他作用,可以讓后續(xù)的維護(hù)者更清楚的知道這是做什么用的。

集成測(cè)試

單元測(cè)試只能測(cè)試一些功能單一的函數(shù),要保證整個(gè)軟件的質(zhì)量?jī)H依賴單測(cè)是不夠的,我們還需要集成測(cè)試。

通常是需要對(duì)外提供服務(wù)的開(kāi)源項(xiàng)目都需要集成測(cè)試:

  • Pulsar
  • Kafka
  • Dubbo 等

以我接觸到的服務(wù)型應(yīng)用主要分為兩類:一個(gè)是 Java 應(yīng)用一個(gè)是 Golang 應(yīng)用。

Golang

Golang 因?yàn)楣ぞ哝湜](méi)有 Java 那么強(qiáng)大,所以大部分的集成測(cè)試的功能都是通過(guò)編寫(xiě) Makefile 和 shell 腳本實(shí)現(xiàn)的。

還是以我熟悉的 Pulsar 的 go-client 為例,它在 GitHub 的集成測(cè)試是通過(guò) GitHub action 觸發(fā)的,定義如下:

最終調(diào)用的是 Makefile 中的 test 命令,并且把需要測(cè)試的 Golang 版本傳入進(jìn)去。

Dockerfile:

這個(gè)鏡像簡(jiǎn)單來(lái)說(shuō)就是將 Pulsar 的鏡像作為基礎(chǔ)運(yùn)行鏡像(這里面包含了 Pulsar 的服務(wù)端),然后將這個(gè) pulsar-client-go 的代碼復(fù)制進(jìn)去編譯。

接著運(yùn)行:

cd /pulsar/pulsar-client-go && ./scripts/run-ci.sh

也就是測(cè)試腳本。

測(cè)試腳本的邏輯也很簡(jiǎn)單:

  • 啟動(dòng) pulsar 服務(wù)端
  • 運(yùn)行測(cè)試代碼 因?yàn)樗械臏y(cè)試代碼里連接服務(wù)端的地址都是 localhost,所以可以直接連接。

通過(guò)這里的 action 日志可以跟蹤所有的運(yùn)行情況。

Java

Java 因?yàn)楣ぞ哝湉?qiáng)大,所以集成測(cè)試幾乎不需要用 Makefile 和腳本配合執(zhí)行。

還是以 Pulsar 為例,它的集成測(cè)試是需要模擬在本地啟動(dòng)一個(gè)服務(wù)端(因?yàn)?Pulsar 的服務(wù)端源碼和測(cè)試代碼都是 Java 寫(xiě)的,更方便做測(cè)試),然后再運(yùn)行測(cè)試代碼。

這個(gè)的好處是任何一個(gè)單測(cè)都可以在本地直接運(yùn)行,而  Go 的代碼還需要先在本地啟動(dòng)一個(gè)服務(wù)端,測(cè)試起來(lái)比較麻煩。

來(lái)看看它是如何實(shí)現(xiàn)的,我以其中一個(gè) BrokerClientIntegrationTest為例:

會(huì)在單測(cè)啟動(dòng)的時(shí)候先啟動(dòng)服務(wù)端。

最終會(huì)調(diào)用 PulsarTestContext 的 build 函數(shù)啟動(dòng) broker(服務(wù)端),而執(zhí)行單測(cè)也只需要使用 mvn test 就可以自動(dòng)觸發(fā)這些單元測(cè)試。

只是每一個(gè)單測(cè)都需要啟停服務(wù)端,所以要把 Pulsar 的所有單測(cè)跑完通常需要 1~2 個(gè)小時(shí)。

以上就是日常編寫(xiě)單測(cè)可能會(huì)碰到的場(chǎng)景,希望對(duì)大家有所幫助。

責(zé)任編輯:姜華 來(lái)源: crossoverJie
相關(guān)推薦

2011-02-21 09:54:14

PHPPHPUnit

2011-08-22 13:57:55

gtest

2017-02-21 10:30:17

Android單元測(cè)試研究與實(shí)踐

2017-01-14 23:42:49

單元測(cè)試框架軟件測(cè)試

2017-05-04 16:35:45

2024-12-02 11:39:30

2022-04-08 09:01:56

腳本Go應(yīng)用單元

2017-05-04 15:36:54

Openstack Q實(shí)現(xiàn)實(shí)踐

2024-05-23 08:02:23

2024-01-09 07:25:31

2021-03-11 12:33:50

JavaPowerMock技巧

2012-10-29 09:45:52

單元測(cè)試軟件測(cè)試測(cè)試實(shí)踐

2011-02-16 09:45:13

PHPPHPUnit

2009-09-01 10:20:06

protected方法單元測(cè)試

2021-09-27 13:02:05

Python技巧測(cè)試

2024-06-28 10:25:18

2010-06-01 15:25:27

JavaCLASSPATH

2016-12-08 15:36:59

HashMap數(shù)據(jù)結(jié)構(gòu)hash函數(shù)

2020-07-21 08:26:08

SpringSecurity過(guò)濾器

2024-10-07 09:12:33

點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)

主站蜘蛛池模板: 亚洲一区二区三区国产 | 国产日韩欧美在线 | 在线视频一区二区 | 久久久九九九九 | 国产精品 亚洲一区 | 久久久久久久国产 | 国产免费视频 | 国产国拍亚洲精品av | 视频在线一区二区 | 毛片一区二区 | 一区二区三区在线播放 | 国产在线视频在线观看 | 午夜视频在线观看网址 | 国产精品毛片一区二区三区 | 亚洲一一在线 | 国产伊人精品 | 久久99国产精一区二区三区 | 91国产在线视频在线 | 久久免费看| 天天天天操 | av毛片 | 高清黄色 | 九九九久久国产免费 | 北条麻妃视频在线观看 | 成人av在线播放 | www午夜视频 | 亚洲精品久久久一区二区三区 | 欧美一区二区三区大片 | 中文字幕在线免费视频 | 国产亚洲精品久久久久动 | 91在线视频在线观看 | 久久免费视频在线 | 91精品国产91久久久久青草 | 天天激情综合 | 91欧美激情一区二区三区成人 | 精品一区精品二区 | 中文字幕 国产精品 | 偷拍自拍在线观看 | 91色视频在线 | 久久久久亚洲av毛片大全 | 国产在线精品一区二区三区 |