基于Appium的Android功能自動(dòng)化實(shí)踐

前言

做Android端功能自動(dòng)化已有2年多的時(shí)間了脸哀,使用過(guò)的功能自動(dòng)化框架有Robotium长豁、Uiautomator檐蚜、Appium。最近研究自動(dòng)化case復(fù)用的方案瓮恭,調(diào)研了Appium的自動(dòng)化框架书蚪,并將其應(yīng)用到銀行一賬通的標(biāo)版中剃袍,本文詳細(xì)介紹基于Appium的Android功能自動(dòng)化實(shí)戰(zhàn)經(jīng)驗(yàn)剥悟。主要包括以下幾方面內(nèi)容:

  • Appium框架原理介紹
  • Appium框架常用API介紹
  • 基于Appium框架的自動(dòng)化開發(fā)環(huán)境搭建
  • 自動(dòng)化case開發(fā)及分層結(jié)構(gòu)設(shè)計(jì)
  • 自動(dòng)化測(cè)試用例書寫規(guī)范及注意事項(xiàng)
  • 功能自動(dòng)化接入持續(xù)集成方案

Android常用功能自動(dòng)化框架比較

下表給出Robotium、Uiautomator蹦骑、Appium三種框架的比較慈省。
了解各個(gè)自動(dòng)化框架的特性,結(jié)合產(chǎn)品自身特點(diǎn)眠菇,選取一個(gè)合適的框架尤為重要辫呻。

Paste_Image.png

Appium框架原理介紹

Appium 是一個(gè)開源清钥、跨平臺(tái)的自動(dòng)化測(cè)試工具,用于測(cè)試原生和輕量移動(dòng)應(yīng)用放闺,支持iOS, Android 和 FirefoxOS 平臺(tái)祟昭。Appium 驅(qū)動(dòng)蘋果的 UIAutomation 庫(kù)和 Android 的 UiAutomator 框架,使用 Selenium 的 WebDriver JSON 協(xié)議怖侦。下圖是Appium在Android端的原理架構(gòu)圖

Paste_Image.png

從圖中可以看出篡悟,Appium Client是對(duì)webdriver原生api的一些擴(kuò)展和封裝,它配合原生的webdriver來(lái)使用匾寝,二者缺一不可搬葬。Appium Server有兩個(gè)主要功能:首先它作為一個(gè)http服務(wù)器端,接收從Appium Client發(fā)送過(guò)來(lái)的命令(可以認(rèn)為是case中的具體操作)艳悔。其次急凰,它作為bootstrap客戶端,在接收Client的命令后猜年,通過(guò)socket方式抡锈,把這些命令發(fā)給目標(biāo)android機(jī)器的bootstrap來(lái)驅(qū)動(dòng)Uiautomator執(zhí)行自動(dòng)化操作。關(guān)于Appium的工作原理乔外,網(wǎng)上資料很多床三,不再詳細(xì)介紹,可參考如下鏈接
http://www.2cto.com/kf/201410/347851.html
http://www.blogjava.net/qileilove/archive/2014/12/23/421659.html

Appium框架常用API介紹

Appium的API包含在Appium Client中杨幼,下表給出不同語(yǔ)言平臺(tái)對(duì)應(yīng)的Client下載地址

Paste_Image.png

下表給出Java平臺(tái)的常用API撇簿,其他API可自行百度或查看Client源碼


Paste_Image.png

Appium元素定位和操作的api是分開的,這點(diǎn)與robotium框架不同差购,與Google新推出的iOS端功能自動(dòng)化框架EarlGrey是類似的四瘫。關(guān)于元素定位的api需要注意:當(dāng)成功定位到元素時(shí),返回WebElement對(duì)象欲逃,但是若不能定位到元素找蜜,此api直接報(bào)錯(cuò),而不是返回NULL暖夭。這對(duì)“判斷某個(gè)控件是否存在”這樣的常用操作經(jīng)常容易出錯(cuò),比如用如下代碼判斷登錄按鈕是否存在撵孤。

public boolean isLoginButtonShow(){
  WebElement wl = DriverManager.getInstance().findElementById(packagename
                    +  ":id/login_input_account");
  return wl.isDisplayed();
 }

當(dāng)?shù)卿洶粹o存在時(shí)迈着,返回true,但若登錄按鈕不存在時(shí)邪码,以上代碼并不會(huì)返回false裕菠,而是在第2行直接崩潰”兆ǎ可通過(guò)try catch捕獲異常奴潘,修改如下:

public boolean isLoginButtonShow(){
    try{
    WebElement wl = DriverManager.getInstance().findElementById(packagename  
                +  ":id/login_input_account");
         return wl.isDisplayed();
  }catch (Exception e){
  return false;
   }
  }

基于Appium框架的自動(dòng)化開發(fā)環(huán)境搭建

萬(wàn)事開頭難旧烧,自動(dòng)化開發(fā)環(huán)境的搭建會(huì)比較麻煩。以下詳細(xì)講解如何在mac os操作系統(tǒng)下画髓,搭建基于Appium的自動(dòng)化開發(fā)環(huán)境掘剪。
1、Android開發(fā)環(huán)境搭建(JDK/SDK/AndroidStudio)請(qǐng)自行百度,所需安裝包可在如下網(wǎng)頁(yè)中下載:
http://tools.android-studio.org
Appium for mac安裝可參考以下連接:
http://www.cnblogs.com/oscarxie/p/3894559.html
其中appium 的安裝奈虾,建議使用dmg安裝夺谁,點(diǎn)擊下載安裝包∪馕ⅲ“npm install -g appium”命令行安裝親測(cè)一直報(bào)錯(cuò)匾鸥,翻墻了也報(bào)錯(cuò)。
2碉纳、AndroidStudio中新建android工程AppDemo勿负。
3、創(chuàng)建java module(appium):new—>new module—>java library劳曹,此java工程用來(lái)開發(fā)自動(dòng)化case奴愉。
4、安裝Appium Client:創(chuàng)建java module (selenium)厚者,libs目錄中導(dǎo)入相關(guān)selenium包躁劣,點(diǎn)擊下載selenium壓縮包
5、appium module添加selenium依賴:file—>project structure—>modelues選中appium—>dependencies添加selenium module库菲。
6账忘、appium module中,新建java class熙宇,開始開發(fā)自動(dòng)化case鳖擒。至此AppDemo工程目錄結(jié)構(gòu)如下所示:

Paste_Image.png

7、啟動(dòng)appium server(命令行或者圖形界面啟動(dòng))烫止,注意蒋荚,啟動(dòng)時(shí)Android Settings中請(qǐng)勾選No Reset選項(xiàng),可防止每次執(zhí)行case時(shí)馆蠕,都重新安裝app期升,如下所示。

Paste_Image.png

8互躬、case執(zhí)行播赁,可通過(guò)IDE和腳本兩種方式執(zhí)行case
IDE方式:case名右擊—>run
腳本方式:auto_run.sh,見下方

#!/bin/bash
#auto_run.sh
source ~/.bash_profile
cd ./AppDemo
gradle clean
gradle build
export CLASSPATH=$APPIUM_HOME/java-client-3.3.0.jar:$APPIUM_HOME/selenium-java-2.48.2-srcs.jar:$APPIUM_HOME/selenium-java-2.48.2.jar:$APPIUM_HOME/junit-4.12.jar:$APPIUM_HOME/hamcrest-core-1.3.jar:$APPIUM_HOME/libs/*  
cd ./appium
java -classpath $CLASSPATH:./build/libs/appium.jar  org.junit.runner.JUnitCore   com.incito.appiumdemo.cases.PersonalCente

其中CLASSPATH需要設(shè)置為Appium Client所在路徑吼渡,
com.incito.appiumdemo.cases.PersonalCenter為case所在類容为,腳本的執(zhí)行方式有利于自動(dòng)化接入持續(xù)集成和平臺(tái)化。
至此基于Appium的自動(dòng)化開發(fā)環(huán)境搭建完成,下一節(jié)介紹如何開發(fā)自動(dòng)化case及case的分層結(jié)構(gòu)設(shè)計(jì)坎背。

基于Appium的自動(dòng)化case開發(fā)及case分層結(jié)構(gòu)設(shè)計(jì)

首先為每條case創(chuàng)建一個(gè)公共的基類AppiumTestBase替劈,內(nèi)含setup和teardown兩個(gè)方法,以后每條case繼承該基類即可得滤。代碼如下:

public class AppiumTestBase {    
  public WebDriverWait webwait;    
private AndroidDriver driver;    
@Before    
public void setUp() throws Exception {        
File classpathRoot = new File(System.getProperty("user.dir"));        
File appDir = new File(classpathRoot, "apps");        
File app = new File(appDir, Config.CURRENT_BANK);        
DesiredCapabilities capabilities = new DesiredCapabilities();        capabilities.setCapability("deviceName",Config.DEVICE_NAME);        capabilities.setCapability(CapabilityType.BROWSER_NAME, "");        capabilities.setCapability(CapabilityType.VERSION, "5.0.1");        capabilities.setCapability("platformName", "android");        
capabilities.setCapability("app", app.getAbsolutePath());        
capabilities.setCapability("udid", Config.DEVICE_NAME);//adb devices獲得的值        
driver = new AndroidDriver(new URL("http://127.0.0.1:"+Config.APPIUM_PORT+"/wd/hub"), capabilities);        
webwait = new WebDriverWait(driver,10);        
DriverManager.init(driver);        
DriverManager.initWebWait(webwait);        
driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);    
}    
@After    
public void tearDown() throws Exception {        
driver.quit();    
}

setup操作包含導(dǎo)入待測(cè)應(yīng)用apk包陨献,設(shè)置與Appium Server連接所需參數(shù),并初始化AndroidDriver對(duì)象耿戚。以標(biāo)版登錄case為例湿故,其需要繼承AppiumTestBase,代碼如下:

package com.incito.appiumdemo;
import org.junit.After;
import org.junit.Before;
import org.openqa.selenium.remote.CapabilityType;
import org.openqa.selenium.remote.DesiredCapabilities;
import org.openqa.selenium.support.ui.WebDriverWait;
import java.io.File;
import java.net.URL;
import java.util.concurrent.TimeUnit;
import io.appium.java_client.android.AndroidDriver;
public class AccountLogin extends AppiumTestBase{    
/**     
* case 編號(hào) AccountLogin_001     
* ****@throws ****Exception     
*/    
@Test    
public void testAccountLogin() throws Exception {        
Account.*getInstance*().gotoLoginPage();        
Account.*getInstance*().doLogin();    
}
/**     
* case 編號(hào) AccountLogin_002     
* ****@throws ****Exception     
*/   
@Test    
public void testLoginout() throws Exception {        
Account.*getInstance*().gotoGestureLoginPage();        
Account.*getInstance*().doGestureLogin(Config.*CURRENT_USERNAME*);        
Personal.*getInstance*().doLogout();    
}

testAccountLogin Case中膜蛔,執(zhí)行了兩步操作:進(jìn)入登錄頁(yè)面坛猪;執(zhí)行登錄操作。這兩步操作都封裝在Account類里面皂股。由此引入自動(dòng)化case的分層設(shè)計(jì)框架墅茉,如下圖:

Paste_Image.png

自動(dòng)化開發(fā)過(guò)程中,經(jīng)常遇到的一個(gè)問(wèn)題是呜呐,隨著產(chǎn)品的不斷更新迭代就斤,APP的UI會(huì)不斷發(fā)生變化,自動(dòng)化如何去應(yīng)對(duì)這樣的變化蘑辑,如何降低其維護(hù)代價(jià)洋机。case分層設(shè)計(jì)主要是基于自動(dòng)化可維護(hù)性的考慮,可維護(hù)性是功能自動(dòng)化最重要的評(píng)價(jià)指標(biāo)之一洋魂,其直接決定了自動(dòng)化是否能開展下去绷旗。試想case數(shù)量達(dá)到一定程度時(shí),若沒有采用封裝副砍、分層的設(shè)計(jì)思路衔肢,極有可能出現(xiàn)“牽一發(fā)而動(dòng)全身”的問(wèn)題。下圖是標(biāo)版自動(dòng)化case的分層目錄圖豁翎。

Paste_Image.png

下面以登錄case為例角骤,詳細(xì)了解其分層調(diào)用關(guān)系。登錄case在cases層的AccountLogin類中心剥,其代碼上面已經(jīng)展示邦尊,回到testAccountLogin Case的操作,第二步操作為Account.getInstance().doLogin();即在登錄頁(yè)面執(zhí)行登錄操作优烧,其封裝在business層的Account類中蝉揍,代碼片段如下:

public void doLogin(){    
LoginUiAction.*getInstance*().enterUsername(Config.*CURRENT_USERNAME*);    LoginUiAction.*getInstance*().enterPassword(Config.*CURRENT_PASSWORD*);    LoginUiAction.*getInstance*().clickLogin();    
HomeUiAction.*getInstance*().clickOnCancelGesture();    
Assert.*assertTrue*(LoginUiAction.*getInstance*().isLogin());
}

doLogin()方法中執(zhí)行了“輸入用戶名;輸入密碼匙隔;點(diǎn)擊登錄按鈕疑苫;判斷登錄是否成功”操作,顯然這幾步操作都在UI層LoginUiAction類中纷责。我們?cè)俨長(zhǎng)oginUiAction.getInstance().
enterUsername()方法中做了什么捍掺,代碼片段如下:

//輸入用戶名
public void enterUsername(String username){    
WebElement wl = DriverManager.*getInstance*().findElementById(packagename +    
      ":id/login_input_account");    
 wl.clear();   
 wl.sendKeys(username);
}

enterUsername()方法中,先根據(jù)id定位用戶名輸入框再膳,然后清空輸入框挺勿,再輸入用戶名,這些操作都是Appium Client中提供的API喂柒。至此演示了登錄case的分層調(diào)用過(guò)程:cases—>business—>ui—>api不瓶。按照分層結(jié)構(gòu),只要業(yè)務(wù)邏輯不變灾杰,case維護(hù)任務(wù)主要放在ui層蚊丐,上層無(wú)需改動(dòng),如此可極大減少case的維護(hù)代價(jià)艳吠。
在doLogin()方法中有一個(gè)“判斷登錄是否成功”的操作麦备,斷言操作是自動(dòng)化測(cè)試用例中必不可少的一部分,下面就開始介紹自動(dòng)化測(cè)試用例的書寫規(guī)范昭娩。

自動(dòng)化測(cè)試用例書寫規(guī)范及注意事項(xiàng)

一條case一般需要包括如下幾個(gè)要素:

  • 數(shù)據(jù)準(zhǔn)備
  • 具體操作
  • 驗(yàn)證點(diǎn)
  • 清楚操作結(jié)果

數(shù)據(jù)準(zhǔn)備指提前準(zhǔn)備測(cè)試賬號(hào)凛篙,假數(shù)據(jù)等;具體操作得按照業(yè)務(wù)測(cè)試同學(xué)提供的文本case來(lái)轉(zhuǎn)化栏渺;驗(yàn)證點(diǎn)指自動(dòng)化操作后呛梆,UI前后的變化點(diǎn),比如登錄后磕诊,首頁(yè)會(huì)出現(xiàn)用戶名填物、總資產(chǎn)、財(cái)富分等控件秀仲,這些都是驗(yàn)證點(diǎn)融痛;清楚操作結(jié)果主要是基于case獨(dú)立性的考慮,盡可能做到每條case是獨(dú)立的神僵,這樣某條case fail了雁刷,也不會(huì)對(duì)其他case造成影響,當(dāng)然這樣會(huì)增加case的粒度保礼,case穩(wěn)定性會(huì)受影響沛励,兩者之間可自行平衡。
下面介紹case開發(fā)的注意事項(xiàng):

  1. 元素定位
  • ID(首選)
  • 文本
  • 控件類型
  • 坐標(biāo)
  1. UI操作需要合理的sleep
  2. case獨(dú)立性
  3. 封裝常用操作
  4. 注釋及case前寫明操作步驟

元素定位有ID/文本/控件類型/坐標(biāo)四種方式炮障,推薦使用ID,因?yàn)橹灰硞€(gè)控件還存在目派,其ID變化的可能性很小,若其位置發(fā)生了變動(dòng)胁赢,case都無(wú)需維護(hù)企蹭。
下面給出查找元素ID的幾種方法:uiautomatorviewer/hierarchyviewer/源碼。uiautomatorviewer和hierarchyviewer工具在Android SDK中,配置好環(huán)境變量后谅摄,直接在命令行輸入命令即可打開圖形化工具徒河,hierarchyviewer需要手機(jī)root,推薦使用uiautomatorviewer送漠。如果有待測(cè)應(yīng)用的源碼顽照,也可以通過(guò)源碼來(lái)找ID,當(dāng)然這種方式比較痛苦闽寡,但對(duì)熟悉業(yè)務(wù)及個(gè)人成長(zhǎng)是有好處的代兵,我最開始就是通過(guò)源碼來(lái)找ID的。
對(duì)于一些奇葩的情況爷狈,元素ID/文本/控件類型都無(wú)法定位時(shí)植影,使用坐標(biāo)定位絕對(duì)好使,但不到萬(wàn)不得已涎永,盡量不用何乎,因?yàn)橛米鴺?biāo)定位就失去了兼容性,換個(gè)手機(jī)土辩,case可能會(huì)執(zhí)行失敗支救。
UI操作需要合理的sleep,經(jīng)常由于網(wǎng)絡(luò)原因拷淘,UI加載需要一定時(shí)間各墨,自動(dòng)化操作過(guò)程中需要合理的sleep,sleep時(shí)間短了启涯,case會(huì)失敗贬堵,長(zhǎng)了又浪費(fèi)時(shí)間。Appium框架中引入了隱式sleep方案结洼,在初始化代碼中添加driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);即可黎做,表示每步操作最多等待10s。
case獨(dú)立性前面已介紹松忍,封裝及注釋容易理解蒸殿,不做過(guò)多解釋。

功能自動(dòng)化接入持續(xù)集成方案

功能自動(dòng)化一般用于項(xiàng)目集中測(cè)試鸣峭、回歸測(cè)試宏所、dailybuild等,我們不可能通過(guò)IDE手動(dòng)來(lái)運(yùn)行case摊溶,一般可借助于jenkins或平臺(tái)化的方式來(lái)批量執(zhí)行case爬骤。下面介紹如何將功能自動(dòng)化接入jenkins。

Paste_Image.png

接入jenkins主要用到了其定時(shí)和輪詢的功能莫换,我們只要準(zhǔn)備好構(gòu)建jar(build.sh)和執(zhí)行case(execCase.sh)的腳本霞玄,放入jenkins的Excute shell模塊骤铃,然后配置定時(shí)或輪詢的時(shí)間即可。build.sh和execCase.sh可參照第四部分介紹的auto_run.sh坷剧。具體執(zhí)行過(guò)程如上圖所示:jenkins觸發(fā)自動(dòng)化job劲厌;拉取構(gòu)建站最新apk,拷貝至appium module的apps目錄下听隐;構(gòu)建測(cè)試工程,生成appium.jar(build.sh)哄啄;批量執(zhí)行case(execCase.sh)雅任,最后jenkins會(huì)輸出自動(dòng)化的執(zhí)行結(jié)果,但是輸出結(jié)果可視化程度不好咨跌,可自行開發(fā)生成報(bào)告腳本沪么。至此詳細(xì)介紹了基于Appium的功能自動(dòng)化開發(fā)全過(guò)程。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末锌半,一起剝皮案震驚了整個(gè)濱河市禽车,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌刊殉,老刑警劉巖殉摔,帶你破解...
    沈念sama閱讀 221,198評(píng)論 6 514
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異记焊,居然都是意外死亡逸月,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,334評(píng)論 3 398
  • 文/潘曉璐 我一進(jìn)店門遍膜,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)碗硬,“玉大人,你說(shuō)我怎么就攤上這事瓢颅《魑玻” “怎么了?”我有些...
    開封第一講書人閱讀 167,643評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵挽懦,是天一觀的道長(zhǎng)翰意。 經(jīng)常有香客問(wèn)我,道長(zhǎng)信柿,這世上最難降的妖魔是什么猎物? 我笑而不...
    開封第一講書人閱讀 59,495評(píng)論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮角塑,結(jié)果婚禮上蔫磨,老公的妹妹穿的比我還像新娘。我一直安慰自己圃伶,他們只是感情好堤如,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,502評(píng)論 6 397
  • 文/花漫 我一把揭開白布蒲列。 她就那樣靜靜地躺著,像睡著了一般搀罢。 火紅的嫁衣襯著肌膚如雪蝗岖。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,156評(píng)論 1 308
  • 那天榔至,我揣著相機(jī)與錄音抵赢,去河邊找鬼。 笑死唧取,一個(gè)胖子當(dāng)著我的面吹牛铅鲤,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播枫弟,決...
    沈念sama閱讀 40,743評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼邢享,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了淡诗?” 一聲冷哼從身側(cè)響起骇塘,我...
    開封第一講書人閱讀 39,659評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎韩容,沒想到半個(gè)月后款违,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,200評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡群凶,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,282評(píng)論 3 340
  • 正文 我和宋清朗相戀三年奠货,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片座掘。...
    茶點(diǎn)故事閱讀 40,424評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡递惋,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出溢陪,到底是詐尸還是另有隱情萍虽,我是刑警寧澤,帶...
    沈念sama閱讀 36,107評(píng)論 5 349
  • 正文 年R本政府宣布形真,位于F島的核電站杉编,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏咆霜。R本人自食惡果不足惜邓馒,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,789評(píng)論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望蛾坯。 院中可真熱鬧光酣,春花似錦、人聲如沸脉课。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,264評(píng)論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至唱遭,卻和暖如春戳寸,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背拷泽。 一陣腳步聲響...
    開封第一講書人閱讀 33,390評(píng)論 1 271
  • 我被黑心中介騙來(lái)泰國(guó)打工疫鹊, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人司致。 一個(gè)月前我還...
    沈念sama閱讀 48,798評(píng)論 3 376
  • 正文 我出身青樓拆吆,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親蚌吸。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,435評(píng)論 2 359

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