給安卓開(kāi)發(fā)小白們的unit test指南 - 這也能測(cè)?這也要測(cè)异赫?

長(zhǎng)久以來(lái)椅挣,測(cè)試對(duì)于很多安卓開(kāi)發(fā)小白們都是一個(gè)盲區(qū)。這個(gè)很大程度上是因?yàn)樽鯽pp祝辣,大家都習(xí)慣了自己手動(dòng)測(cè)試feature贴妻,畢竟是所見(jiàn)即所得的東西切油,點(diǎn)幾個(gè)按鈕看看能不能按照要求展示幾個(gè)頁(yè)面好像并不是那么難蝙斜。其次是因?yàn)楹芏啻a寫(xiě)的并不是特別可測(cè) (比如代碼都寫(xiě)在activity里面),導(dǎo)致沒(méi)法進(jìn)行單元測(cè)試澎胡。

以上的幾個(gè)原因孕荠,最終導(dǎo)致了很多接觸安卓開(kāi)發(fā)沒(méi)多久的朋友(尤其是在小廠娩鹉,對(duì)迭代速度要求更快的地方)沒(méi)怎么接觸過(guò)安卓的單元測(cè)試,也不知道test coverage是什么稚伍,更加意識(shí)不到單元測(cè)試的重要性弯予。產(chǎn)生了一種類似于咱們大學(xué)剛接觸高等數(shù)學(xué)證明題的感覺(jué):

Screenshot 2021-05-02 at 10.56.22 AM.png

對(duì)應(yīng)到咱們今天講的測(cè)試,很多人在看完同事寫(xiě)的測(cè)試代碼之后也有類似的震驚个曙。"這還要測(cè)锈嫩?" “這也能測(cè)?”

download.jpeg

今天我想著重講一下安卓開(kāi)發(fā)中單元測(cè)試的意義垦搬,來(lái)說(shuō)明“這也要測(cè)”的意義呼寸。同時(shí)提供一些安卓測(cè)試中的小技巧,把“這也能測(cè)”的問(wèn)題一并給解決 :)

單元測(cè)試的意義

對(duì)于安卓開(kāi)發(fā)來(lái)說(shuō)猴贰,大部分小白們對(duì)于單元測(cè)試處于懵逼的狀態(tài)对雪。不知道測(cè)試有啥用。我剛剛開(kāi)始工作的時(shí)候就特別不喜歡寫(xiě)test米绕,覺(jué)得是浪費(fèi)時(shí)間瑟捣。我的想法是,就算單元測(cè)試成功了栅干,你的app跑起來(lái)也不一定能work啊迈套。。碱鳞。 所以還不如專心在手動(dòng)測(cè)試上交汤。

其實(shí)這個(gè)想法也不能說(shuō)完全錯(cuò),甚至可以說(shuō)是對(duì)了一半劫笙。因?yàn)閱卧獪y(cè)試成功不一定代碼app的功能就沒(méi)問(wèn)題芙扎。但是反過(guò)來(lái)說(shuō),如果單元測(cè)試都不對(duì)填大,那app的功能肯定有問(wèn)題戒洼。

軟件開(kāi)發(fā)中有一個(gè)大假設(shè),就是如果你的每個(gè)模塊都能自己獨(dú)立且正確運(yùn)行的話允华,這個(gè)軟件就大概率能正確的運(yùn)行圈浇。比如,如果我們app中每個(gè)class都能通過(guò)各種的獨(dú)立單元測(cè)試靴寂,那么把他們拼接起來(lái)這個(gè)app應(yīng)該就沒(méi)毛病了磷蜀。

1_nrKara4sTWMOpJRTWRCJ6Q.png

單元測(cè)試位于軟件開(kāi)發(fā)測(cè)試的金字塔的最底層,也是最重要的那一層百炬。單元測(cè)試都跑不過(guò)褐隆,就別談集成測(cè)試 , UI 測(cè)試了剖踊。安卓也不例外庶弃。

可能光這么說(shuō)大家還是體會(huì)不到單元測(cè)試的好處衫贬。那么我就選一個(gè)方面來(lái)具體的說(shuō)說(shuō)單元測(cè)試在實(shí)際開(kāi)發(fā)過(guò)程中,可以給我們帶來(lái)什么好處歇攻。

怎么改固惯?單元測(cè)試說(shuō)了算

在大廠工作的朋友肯定都有過(guò)接手別人項(xiàng)目的經(jīng)驗(yàn),當(dāng)你在嘗試修改某一個(gè)class的時(shí)候缴守,你怎么確定你添加的代碼就是對(duì)的呢 (在不運(yùn)行app做手動(dòng)測(cè)試之前)葬毫?

答案就是單元測(cè)試。unit test在很多情況下屡穗,可以當(dāng)做你修改代碼的規(guī)則. class A 哪里改了會(huì)影響到class B供常,都可以在跑unit test之后發(fā)現(xiàn),這也是你作為一個(gè)項(xiàng)目后來(lái)者了解細(xì)節(jié)的方式鸡捐。

用一個(gè)我以前自己類似的經(jīng)歷做例子栈暇。假如有以下MVP pattern的代碼:

class Presenter{
    enum Status{
       LARGE,
       MEDIUM
    }
    
    public Status getFinancialStatus(int size){
        if(size < 1000){
           return Status.SMALL
        }
        else{
           return Status.MEDIUM
        }
    }
}

以上代碼通過(guò)房子size大小判斷是small還是medium。現(xiàn)在產(chǎn)品經(jīng)理說(shuō)咱給他添加一個(gè)large的size把箍镜。于是你興高采烈改了代碼源祈,簡(jiǎn)單的很,不就是加一個(gè)if else么:

class Presenter{
    enum Status{
       LARGE,
       SMALL,
       MEDIUM
    }
    
    public Status getFinancialStatus(int size){
        if(size < 1000){
           return Status.SMALL
        }
        else if( size < 6000){
           return Status.MEDIUM
        }
        else{
           return Status.LARGE
        }
    }
}

結(jié)果app跑起來(lái)之后crash了色迂!

仔細(xì)一看香缺,原來(lái)Activity里面有這樣的代碼(這里的例子都只是模擬場(chǎng)景,為的是說(shuō)明測(cè)試的重要性歇僧,現(xiàn)實(shí)開(kāi)發(fā)中肯定不可能把這種條件判斷寫(xiě)在activity里面)

class HouseActivity extends Activity{
    public void display(int size){
       if(size > 10000){
          throws IllegalStatusException()
       }
       Status status = presenter.getFinancialStatus(size)
       .....其他邏輯
    }
}

HouseActivity 的單元測(cè)試長(zhǎng)這個(gè)樣子:

@Test(expected = IndexOutOfBoundsException.class)
public void sizeTooLargeAssertException(){
    activity.display(30000)
}

原來(lái)我們?cè)赼ctivity里面有邏輯图张,限制最多只能展示大于10000的size,如果我在運(yùn)行app之前就已經(jīng)實(shí)現(xiàn)跑過(guò)了HouseActivity 的單元測(cè)試,我就會(huì)提前知道原來(lái)我們的app不處理大于10000的數(shù)據(jù)诈悍。

以上只是一個(gè)簡(jiǎn)單的例子祸轮,但是這個(gè)例子說(shuō)明了一個(gè)很大的問(wèn)題,就是在提交你的代碼之前侥钳,運(yùn)行一個(gè)有效的單元測(cè)試是有多么重要适袜。他可以幫你測(cè)試修改的代碼會(huì)對(duì)其他模塊有什么影響,如果破壞了既有的測(cè)試(規(guī)則)舷夺,你應(yīng)該怎么處理苦酱。要知道很多代碼在修改之后,你以為你打開(kāi)app手動(dòng)測(cè)試一下通過(guò)了肯定就沒(méi)問(wèn)題给猾,但是你有沒(méi)有想過(guò)疫萤,這個(gè)代碼,這個(gè)類敢伸,會(huì)不會(huì)對(duì)其他頁(yè)面有影響扯饶。這個(gè)就是單元測(cè)試的作用:

制定一套既有的規(guī)則,所有新增/修改的代碼要按照這個(gè)規(guī)則來(lái)運(yùn)行。

測(cè)試這種規(guī)則帝际,要比你手動(dòng)打開(kāi)app測(cè)試更加健壯且快速(compile 一個(gè)完整app vs 運(yùn)行 一個(gè)純java的測(cè)試)。

在理想狀態(tài)下饶辙,每一個(gè)類的每一行代碼都要被unit test cover蹲诀,一套單元測(cè)試的coverage(覆蓋率)可以體現(xiàn)你給你代碼制定規(guī)則的數(shù)量和健壯程度。比如說(shuō)還是用上面的例子:

class Presenter{
    enum Status{
       LARGE,
       MEDIUM
    }
    
    public Status getFinancialStatus(int size){
        if(size < xxx){
           return Status.SMALL
        }
        else{
           return Status.MEDIUM
        }
    }
}

你的測(cè)試如果只有:

@Test
public void smallSizeReturnSmallStatus(){
    int size = 90
    
    assertThat(presenter.getFinancialStatus(size)).isEqualTo(Status.SMALL)
}

那你對(duì)Presenter這個(gè)類的coverage只有50%弃揽。為什么脯爪?因?yàn)槟愕膖est沒(méi)有覆蓋到else這個(gè)語(yǔ)句,補(bǔ)上一下測(cè)試:

@Test
public void largeSizeReturnLargeStatus(){
    int size = 3000
    
    assertThat(presenter.getFinancialStatus(size)).isEqualTo(Status.MEDIUM)
}

跑完這兩個(gè)測(cè)試矿微,你的presener的單元測(cè)試覆蓋率就是100%了痕慢,恭喜!

順便說(shuō)一句涌矢,現(xiàn)在android studio已經(jīng)支持顯示unit test 覆蓋率了,有興趣可以看看

https://developer.android.com/studio/test

比如我有個(gè)dummy class

Screenshot 2021-05-02 at 3.42.08 PM.png

給它的else語(yǔ)句加個(gè)test:

Screenshot 2021-05-02 at 3.41.30 PM.png

Android Studio不僅會(huì)給出覆蓋率等重要數(shù)據(jù)疙驾,還會(huì)給代碼加上標(biāo)記悍募,這樣開(kāi)發(fā)者就可以輕易的看出來(lái)哪一行代碼沒(méi)有被測(cè)試覆蓋,是否需要加測(cè)試(原諒色代表被覆蓋,紅色代表沒(méi)有被覆蓋)

Screenshot 2021-05-02 at 3.41.37 PM.png

測(cè)什么金拒?

我們都知道一個(gè)類的單元測(cè)試是要保證這個(gè)類能正常運(yùn)行。那么什么是類能正常運(yùn)行呢兰珍?這個(gè)標(biāo)準(zhǔn)是什么温亲?

還是以例子為主:

class HousePagePresenter{
   
   //http service client
    private HouseApiService service = new HouseApiService()
    
    public Data getHouseData(int size){
        if(size < 1000){
           return service.call(Status.SMALL)
        }
        else{
           return service.call(Status.MEDIUM)
        }
    }
}

HouseApiService 是一個(gè)做http call的類,參數(shù)是Status匕得。當(dāng)size小于1000就傳SMALL继榆,反之MEDIUM。那對(duì)于HousePagePresenter來(lái)說(shuō)汁掠,這個(gè)類怎么樣運(yùn)行才是正確的略吨?

那就是當(dāng)getHouseData() 傳入的參數(shù)小于1000的時(shí)候,service 類成員要調(diào)用call 方法考阱,而且參數(shù)是SMALL晋南,反之是MEDIUM。

HousePagePresenter只需要保證在合適的size的前提下羔砾,service能調(diào)用call并且使用正確的Status就行了负间。我們只在乎service有沒(méi)有做出正確的動(dòng)作,至于動(dòng)作結(jié)果姜凄,不重要政溃!

怎么測(cè)?

那說(shuō)回來(lái)态秧,這個(gè)怎么測(cè)董虱?

首先,要給一個(gè)代碼做測(cè)試,要先保證他是可測(cè)的愤诱。上面的代碼其實(shí)是沒(méi)法測(cè)試的云头!Not testable.因?yàn)镠ouseApiService作為私有對(duì)象,我們沒(méi)辦法模擬(Mock)它淫半,從而無(wú)法驗(yàn)證它的行為在一定條件下是否符合我們的期望溃槐。

正確的做法是,要做“依賴注入”科吭。把HousePagePresenter對(duì)因?yàn)镠ouseApiService的依賴昏滴,從類對(duì)象的方式轉(zhuǎn)移成別的方式,或者說(shuō)可測(cè)的方式对人,比如移到構(gòu)造函數(shù)里面(也可以通過(guò)別的方式比如說(shuō)setter)谣殊。

class HousePagePresenter{
   
   public HousePagePresenter(HouseApiService service){
     this.service = service
   }
   
   //http service client
    private HouseApiService service;
    
    public Data getHouseData(int size){
        if(size < 1000){
           return service.call(Status.SMALL)
        }
        else{
           return service.call(Status.MEDIUM)
        }
    }
}

這樣的好處可以說(shuō)是非常大。這樣牺弄,我們?cè)跍y(cè)試HousePagePresenter類的時(shí)候姻几,就不需要真正的創(chuàng)建一個(gè)HouseApiService了,而是可以模擬:

@Test
public void smallSizeServiceCall(){
    int size = 900
    HouseApiService service = mock(service.class)
    HousePagePresenter presenter = new HousePagePresenter(service)
    
    presenter.getHouseData(size)
    
    //驗(yàn)證service是不是真正調(diào)用了call势告,并且參數(shù)也是期望值
    verify(service).call(Status.SMALL)
}

通過(guò)把Service移到構(gòu)造函數(shù)鲜棠,讓代碼可以通過(guò)mockito mock的方式生成一個(gè)模擬的Service,這個(gè)service不會(huì)做任何真正的http call培慌,只會(huì)記錄自己call()方法被調(diào)用的情況豁陆。這就夠了,這已經(jīng)能證明HousePagePresenter這個(gè)類沒(méi)問(wèn)題吵护,如果service有問(wèn)題盒音,那應(yīng)該在service自己的單元測(cè)試?yán)锩娼鉀Q。

具體怎么解決依賴注入馅而,可以稍微看一下一個(gè)視頻

https://www.bilibili.com/video/BV1e54y1S72A/?spm_id_from=333.788.recommend_more_video.-1

有人覺(jué)得只有用dagger這類依賴注入庫(kù)才叫依賴注入祥诽,這是一個(gè)常見(jiàn)的誤解。想了解更多的朋友可以自行搜索一下瓮恭。

安卓控件沒(méi)法測(cè)雄坪?

很多朋友會(huì)說(shuō)自己有很多邏輯需要安卓本身的控件支持,這部分真的沒(méi)法測(cè)啊屯蹦。乖乖维哈,谷歌已經(jīng)給我們提供了從UI到系統(tǒng)api的全家桶,想偷懶不寫(xiě)test登澜?不存在的阔挠。。脑蠕。

純UI的單元測(cè)試

對(duì)于fragment 和 activity本身的UI測(cè)試购撼,Roboletric 框架提供了ActivityRule支持跪削,允許開(kāi)發(fā)者在unit test中啟動(dòng)測(cè)試activity,從而啟動(dòng)fragment迂求。同時(shí)配合Espresso框架可以再unit test代碼中獲取View對(duì)象碾盐,達(dá)到測(cè)試View的目的。

比如::

//設(shè)置測(cè)試activity類
private activityScenarioRule = ActivityScenarioRule(TestActivity.class)
@Before
void setup{
   //啟動(dòng)測(cè)試fragment
   activityScenarioRule.scenario.onActivity{     
      activity.setFragmemnt(new TestFragment());
   }
}

@Test
void whenButtonClicked_executeMethod(){
// 通過(guò)onView獲取button揩局,手動(dòng)模擬點(diǎn)擊事件
   onView(R.id.button).performClick();
   verify(presenter).getHouseData()
}

結(jié)合ActivityScenarioRule和Espresso毫玖,我們可以把Fragment或者Activity當(dāng)成一個(gè)正常的再正常不過(guò)的類來(lái)進(jìn)行測(cè)試了。我剛剛?cè)肼毠雀璧臅r(shí)候就想偷懶不給UI寫(xiě)test谐腰,找借口說(shuō)UI測(cè)不了孕豹,直接被senior大哥焦作人涩盾。十气。。

系統(tǒng)API

假如你的方法里需要獲得當(dāng)前手機(jī)運(yùn)營(yíng)商信息春霍,那你可能需要TelephonyManager這個(gè)系統(tǒng)api來(lái)幫忙砸西。

fun getCarrierId(){
        val manager: TelephonyManager = applicationContext.getSystemService(TelephonyManager::class.java)
        if(manager.simCarrierId == 1){
            //做什么邏輯
        }
        else{
            //做其他邏輯
        }
    }

這種情況,你需要Shadow object來(lái)幫忙啦址儒!

Roboletric 提供各種系統(tǒng)級(jí)別API的shadow芹枷,幫助你在測(cè)試的時(shí)候模擬不同的其情況。

比如:


@Test
fun testCarrierId(){
   val shadowTelephonyManager  = Shadows.of(context.getSystemService(TelephonyManager::class::java))

   //給shadow強(qiáng)行設(shè)置一個(gè)值
   
   shadowTelephonyManager.setSimCarrierId(-1);
   
   //繼續(xù)測(cè)試getCarrierId()方法
}

通過(guò)shadow莲趣,我們就可以測(cè)試那些含有系統(tǒng)級(jí)別api的類和方法了鸳慈。

有了Roboletric之后,以前那些復(fù)雜的UI喧伞,和系統(tǒng)api測(cè)試再也不是問(wèn)題了走芋。我寫(xiě)了這么久測(cè)試之后發(fā)現(xiàn),基本上沒(méi)有不可以shadow潘鲫,或者不能mock的東西了翁逞。每當(dāng)我發(fā)現(xiàn)自己的代碼的某一行測(cè)不了,那肯定是我的代碼沒(méi)有寫(xiě)成可測(cè)的形式溉仑。

結(jié)尾

來(lái)谷歌的這幾個(gè)月可以說(shuō)我在各種被教做人挖函,知識(shí)非常匱乏。谷歌在測(cè)試浊竟,和代碼規(guī)范方面比之前亞麻可以說(shuō)嚴(yán)了不只一個(gè)級(jí)別怨喘,第一個(gè)月我就打破了自己修改代碼的記錄,一個(gè)PR修改了30次振定。哲思。。

但是被教做人的同時(shí)我也學(xué)到了不少吩案,尤其是unit test棚赔。以前在亞麻和創(chuàng)業(yè)公司隨意慣了,寫(xiě)測(cè)試?不存在的靠益。丧肴。。在寫(xiě)崩組內(nèi)系統(tǒng)次數(shù)逐漸增加之后胧后,我也漸漸意識(shí)到了單元測(cè)試的重要性芋浮,也想著趁腦子還有貨和大家多分享一下,也請(qǐng)各位大牛多多指正壳快!

祝大家五一快樂(lè)纸巷!羨慕國(guó)內(nèi)的朋友已經(jīng)到處游山玩水了。眶痰。瘤旨。美帝疫情還是一天新增好幾萬(wàn)。竖伯。存哲。。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末七婴,一起剝皮案震驚了整個(gè)濱河市祟偷,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌打厘,老刑警劉巖修肠,帶你破解...
    沈念sama閱讀 206,839評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異户盯,居然都是意外死亡嵌施,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)先舷,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)艰管,“玉大人,你說(shuō)我怎么就攤上這事蒋川∩螅” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,116評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵捺球,是天一觀的道長(zhǎng)缸浦。 經(jīng)常有香客問(wèn)我,道長(zhǎng)氮兵,這世上最難降的妖魔是什么裂逐? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,371評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮泣栈,結(jié)果婚禮上卜高,老公的妹妹穿的比我還像新娘弥姻。我一直安慰自己,他們只是感情好掺涛,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,384評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布庭敦。 她就那樣靜靜地躺著,像睡著了一般薪缆。 火紅的嫁衣襯著肌膚如雪秧廉。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,111評(píng)論 1 285
  • 那天拣帽,我揣著相機(jī)與錄音疼电,去河邊找鬼。 笑死减拭,一個(gè)胖子當(dāng)著我的面吹牛蔽豺,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播峡谊,決...
    沈念sama閱讀 38,416評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼茫虽,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼刊苍!你這毒婦竟也來(lái)了既们?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,053評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤正什,失蹤者是張志新(化名)和其女友劉穎啥纸,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體婴氮,經(jīng)...
    沈念sama閱讀 43,558評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡斯棒,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,007評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了主经。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片荣暮。...
    茶點(diǎn)故事閱讀 38,117評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖罩驻,靈堂內(nèi)的尸體忽然破棺而出穗酥,到底是詐尸還是另有隱情,我是刑警寧澤惠遏,帶...
    沈念sama閱讀 33,756評(píng)論 4 324
  • 正文 年R本政府宣布砾跃,位于F島的核電站,受9級(jí)特大地震影響节吮,放射性物質(zhì)發(fā)生泄漏抽高。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,324評(píng)論 3 307
  • 文/蒙蒙 一透绩、第九天 我趴在偏房一處隱蔽的房頂上張望翘骂。 院中可真熱鬧壁熄,春花似錦、人聲如沸碳竟。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,315評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)瞭亮。三九已至方仿,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間统翩,已是汗流浹背仙蚜。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,539評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留厂汗,地道東北人委粉。 一個(gè)月前我還...
    沈念sama閱讀 45,578評(píng)論 2 355
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像娶桦,于是被迫代替她去往敵國(guó)和親贾节。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,877評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容