Web UI自動化

Web UI自動化

1辣垒、規(guī)劃,安排

  • 1.1 什么項目適合做自動化

    a. 項目周期要長摔认,是否需要對這個項目進行長期維護(維優(yōu))狼荞,是否需要長期進行迭代,交付闽寡。
    b. 項目團隊的能力代兵,團隊大小

  • 1.2 什么時候開始做?

    a. 絕大部分的功能都基本已經(jīng)穩(wěn)定了爷狈,界面植影,需求沒有太多變化了
    (初期的幾個版本基本穩(wěn)定下來的之后,所以初期的幾個版本重心點都放在功能涎永,接口思币,性能等方面)
    b. 哪些功能已經(jīng)穩(wěn)定了鹿响,先實現(xiàn)自動化腳本的開發(fā)
    (在版本間歇期開發(fā))

1、搭建自動化測試環(huán)境

  • 框架:python+selenium+unittest框架

  • 步驟

    1谷饿、安裝Python3.x 配置環(huán)境變量

    2惶我、安裝PyCharm

    3、安裝測試庫

  pip install selenium

? 4博投、安裝瀏覽 chrome,firefox,Ie....

? 5绸贡、安裝瀏覽器驅(qū)動

? 備注:瀏覽器驅(qū)動與瀏覽器的版本一定要匹配

? 把瀏覽器驅(qū)動chromedriver.exe放入到python的安裝目錄

2、編寫腳本

2.1毅哗、確定測試范圍:

Web自動化的覆蓋率80-90%,哪些功能有實現(xiàn)了自動化的听怕。

方維項目Web自動化

前端:注冊,登錄虑绵,首頁尿瞭,查詢標的,實名認證蒸殿,借款筷厘,投資,提現(xiàn)宏所,充值酥艳,還款
后臺:登錄,首頁爬骤,貸款管理充石,理財管理,會員管理霞玄,資金管理骤铃,

2.2、確定測試框架:

python+selenium+unittest框架

2.3坷剧、寫web UI自動化基礎腳本的思路惰爬,流程:

說明:我們其實都是通過調(diào)用selenium庫中的webdriver類中的API函數(shù)來實現(xiàn)對頁面元素進行操作,具體都是根據(jù)功能用例的操作步驟來實現(xiàn)自動化腳本的編寫惫企。并針對結(jié)果進行斷言判斷結(jié)果是否與預期結(jié)果一致撕瞧。

1、導包
2狞尔、實例化一個Webdriver對象
3丛版、加載頁面/網(wǎng)址
4、對頁面進行操作偏序,定位元素页畦,操作元素(根據(jù)功能用例中的操作步驟來的)
5、斷言—檢查結(jié)果(檢查點要全面)

備注:Web UI自動化基本腳本最核心關(guān)鍵的是:頁面元素的定位研儒,操作豫缨,還有就是斷言独令,預期結(jié)果一定要檢查到位,全面州胳。調(diào)試

#百度搜索
#用例:驗證精確搜索记焊,搜索selenium
#操作步驟:1. 輸入要搜索的數(shù)據(jù)  2. 點擊百度一下
from selenium import webdriver
import re

#1. 實例化一個Webdriver對象,打開瀏覽器
driver = webdriver.Chrome()

#2. 加載頁面
driver.get("http://www.baidu.com")

#3. 對頁面進行操作栓撞,定位元素遍膜,操作元素(根據(jù)功能用例中的操作步驟來的)
#說明:webdriver類,webdriver對象中提供了各種的API函數(shù)瓤湘,我們都是通過調(diào)用這些API函數(shù)來實現(xiàn)對頁面元素的定位
element1 = driver.find_element_by_id('kw')
element2 = driver.find_element_by_id('su')

element1.send_keys('selenium')
element2.click()

#4. 斷言
#1. 跳轉(zhuǎn)之后的頁面title應該為:selenium_百度搜索
#2. 跳轉(zhuǎn)之后的頁面的body中應該包含selenium相關(guān)的信息
try:
    assert driver.title=='selenium_百度搜索','頁面title與預期結(jié)果不一致瓢颅!'
    assert 'selenium' in re.findall('<body(.+?)</body>',driver.page_source)[0],'頁面的body中不存在selenium信息!'
    print('百度搜索-精確搜索selenium用例,測試通過弛说!')
except AssertionError as e:
    print('百度搜索-精確搜索selenium用例挽懦,測試不通過! %s' %e)

3、元素定位

3.1木人、頁面元素定位的8種基本方式:

原則:有ID盡量用id定位信柿,盡量避免用name,class name定位醒第,因為這兩個可能在壓面會有重復名字渔嚷。

? 如果沒有id,優(yōu)先選擇使用xpath,css_selector,link_text進行定位稠曼。

? 一般xpath形病,css_selector成功概率高。

  • id定位:

    driver.find_element_by_id()
    
  • link text定位:

    driver.find_element_by_link_text()
    
  • xpath定位:拷貝

    driver.find_element_by_xpath()
    
  • css selector選擇器定位: 拷貝

    driver.find_element_by_css_selector()
    
  • name名字定位:

    driver.find_element_by_name()
    
  • class name名字定位:

    driver.find_element_by_class_name()
    
  • tag標簽定位:

    driver.find_element_by_tag_name()
    
  • partial_link_text模糊定位:

    driver.find_element_by_partial_link_text()
    
#用例:驗證正常登錄用例
#功能用例的操作步驟:1. 輸入用戶名  2. 輸入密碼  3. 點擊的登錄  4. 點擊取消
#功能用例的預期結(jié)果:1. 跳轉(zhuǎn)到首頁  2. 首頁上顯示對應昵稱
import time

from selenium import webdriver

#1. 實例化一個webdriver對象
driver = webdriver.Chrome()
driver.implicitly_wait(10)

#2. 加載頁面
driver.get('http://localhost/fw')

#3. 根據(jù)功能用例的操作步驟霞幅,對頁面元素進行定位并操作
#3.1 定位用戶名輸入框漠吻,輸入用戶名
driver.find_element_by_id('login-email-address').send_keys('jason')

#3.2 定位密碼輸入框,輸入密碼
driver.find_element_by_id('login-password').send_keys('zgp123456')

#3.3 定位登錄按鈕司恳,點擊登錄按鈕
driver.find_element_by_id('Iajax-login-submit').click()

#time.sleep(1)

#3.4 定位取消按鈕途乃,點擊取消按鈕
driver.find_element_by_xpath('//*[@id="fanwe_msg_box"]/table/tbody/tr/td[2]/div[3]/input[2]').click()

time.sleep(1)

#4. 斷言,檢查實際結(jié)果與預期結(jié)果是否一致
#4.1 考慮頁面跳轉(zhuǎn)是否正常扔傅,a. 檢查頁面title  b. 檢查頁面url  頁面如果沒有發(fā)生跳轉(zhuǎn)欺劳,檢查頁面上有哪些新變化,去檢查核心信息
page_element_info = driver.find_element_by_xpath('/html/body/div[2]/div/div[2]/div[1]/div/div[1]/span/span').text
print(page_element_info)
try:
    assert page_element_info=='jason','頁面信息校驗失斍稹!'
    print('方維登錄-正常登錄用例枫弟,測試通過邢享!')
except AssertionError as e:
    print('方維登錄-正常登錄用例,測試不通過淡诗! %s' %e)
finally:
    driver.close()
  • xpath定位:

一般直接通過f12拷貝xpath路徑

//*[@id='login-email-address']
//*[@id="header"]/div[2]/div/ul/li[3]/a
//*[@class="ui-select-selected"]
driver.find_element_by_xpath('//*[@id="header"]/div[2]/div/ul/li[3]/div/a[3]').click()   # 點擊申請貸款
#driver.find_element_by_link_text('申請借款').click()
driver.find_element_by_xpath('//*[@id="borrowlb"]/div/ul/li[2]/div[3]/a/img').click()     # 點擊購房借款 (適用)
driver.find_element_by_xpath('//*[@id="J_save_deal_form"]/div[1]/div[5]/dl/dt').click()   # 點擊借款用途
driver.find_element_by_xpath('//*[@class="ui-select-drop"]/a[2]').click()                 # 點擊購房借款
driver.find_element_by_xpath('//*[@id="borrowtitle"]').send_keys('購房首付款')              # 輸入借款標題
driver.find_element_by_xpath('//*[@id="J_save_deal_form"]/div[1]/div[7]/dl/dt').click()   # 點擊有無抵押
driver.find_element_by_xpath('//*[@id="J_save_deal_form"]/div[1]/div[7]/dl/dd/a[2]').click()    #點擊有
  • css Selector選擇器:

<input type="text" value="" name="borrowamount" id="borrowamount" class="f-input ui-textbox normal" init="init">

一個標簽元素的樣式可以使用id樣式骇塘,也可以使用class樣式,id樣式用#表示伊履,class樣式用.表示

driver.find_element_by_css_selector('#borrowamount').send_keys(5000)       # 輸入借款金額
driver.find_element_by_css_selector('form#J_save_deal_form > div:nth-child(1) > div:nth-child(11) > dl > dt').click()   #點擊還款周期
driver.find_element_by_css_selector('form#J_save_deal_form > div:nth-child(1) > div:nth-child(11) > dl > dd >a:nth-child(2)').click()   #點擊按月還款driver.find_element_by_css_selector('#apr').send_keys(15)
driver.find_element_by_css_selector('form#J_save_deal_form > div:nth-child(1) > div:nth-child(18) > dl > dt').click()  #點擊還款方式
driver.find_element_by_css_selector('form#J_save_deal_form > div:nth-child(1) > div:nth-child(18) > dl > dd >a:nth-child(3) ').click()

3.2、滾動條的處理

selenium并不是萬能的款违,有時候頁面上操作無法實現(xiàn)的唐瀑,這時候就需要借助JS來完成了

當頁面上的元素超過一屏后,想操作屏幕下方的元素插爹,是不能直接定位到哄辣,會報元素不可見的。這時候需要借助滾動條來拖動屏幕赠尾,使被操作的元素顯示在當前的屏幕上力穗。滾動條是無法直接用定位工具來定位的。selenium里面也沒有直接的方法去控制滾動條气嫁,這時候只能借助Js了当窗,還好selenium提供了一個操作js的方法:execute_script(),可以直接執(zhí)行js的腳本

一. 控制滾動條高度

1.1 滾動條回到頂部:

js="var q=document.getElementById('id').scrollTop=0"
driver.execute_script(js)

1.2 滾動條拉到底部

js="var q=document.documentElement.scrollTop=10000"
driver.execute_script(js)

1.3 滾動到指定位置

target = driver.find_element_by_id("id_keypair")
driver.execute_script("arguments[0].scrollIntoView();", target)

5寸宵、scrollTo函數(shù)

--scrollHeight 獲取對象的滾動高度崖面。

--scrollLeft 設置或獲取位于對象左邊界和窗口中目前可見內(nèi)容的最左端之間的距離。

--scrollTop 設置或獲取位于對象最頂端和窗口中可見內(nèi)容的最頂端之間的距離梯影。

--scrollWidth 獲取對象的滾動寬度巫员。

  • *#滾動到底部*
js = "window.scrollTo(0,document.body.scrollHeight)"

driver.execute_script(js)
  • *#滾動到頂部*
js = "window.scrollTo(0,0)"

driver.execute_script(js)

3.3、select下來列表定位:**

<select name="gpc">
    <option value="stf" selected="selected">全部時間</option>
    <option value="stf=1564625920.617,1564712320.617|stftype=1">最近一天</option>
    <option value="stf=1564107520.617,1564712320.617|stftype=1">最近一周</option>
    <option value="stf=1562033920.617,1564712320.617|stftype=1">最近一月</option>
    <option value="stf=1533176320.617,1564712320.617|stftype=1">最近一年</option>
</select>

方法一:先點擊父元素光酣,彈出下來列表之后疏遏,然后在點擊子元素

driver.find_element_by_xpath('//*[@id="adv-setting-4"]/select').click()
driver.find_element_by_xpath('//*[@id="adv-setting-4"]/select/option[3]').click()

方法二:先定位父元素,再定位子元素

select_element = driver.find_element_by_xpath('//*[@id="adv-setting-4"]/select')
select_element.find_element_by_xpath('//*[@id="adv-setting-4"]/select/option[3]').click()

方法三:使用selector類

#1. 實例化一個Select對象
select = Select(driver.find_element_by_xpath('//*[@id="adv-setting-4"]/select'))
#select.select_by_value("stf=1564107520.617,1564712320.617|stftype=1")
#select.select_by_index(2)
select.select_by_visible_text('最近一周')

3.4救军、動態(tài)id

注意:以后一旦碰到id帶有數(shù)字的财异,一般這種ID可能是動態(tài)變化的。

對于動態(tài)ID元素定位的方式不能使用ID來定位唱遭,可以使用以下幾種方式:

1戳寸、xpath路徑的方式來定位,但是這個xpath路徑不能是使用跟這個動態(tài)ID有關(guān)聯(lián)的路徑拷泽。比如:

//*[@id="img_out_306500039"]                #不可以
//*[@id="qlogin_list"]/a/span[4]            #xpath路徑從上一級開始進行逐級搜索疫鹊,這個是可以的。
//*[@class="img_out"]                       #保證class是唯一的司致,這個也可以的

2拆吆、通過css selector選擇器來定位,但css selector不能使用跟這個動態(tài)ID相關(guān)的,ID樣式

? 通過樣式脂矫,不能使用跟這個動態(tài)ID相關(guān)的,ID樣式

? 通過屬性枣耀,不能使用動態(tài)ID屬性

driver.find_element_by_css_selector(#img_out_306500039) #使用了動態(tài)ID的樣式,不可以
#driver.find_element_by_css_selector(span.img_out)  #使用了class樣式庭再,可以捞奕,但是class要唯一
driver.find_element_by_css_selector(div[id="qlogin_list"]/a/span[4][class="img_out"])

3牺堰、先定位父元素,再定位子元素

parent_element = driver.find_element_by_xpath('//*[@id="qlogin_list"]/a[1]')  #定位父元素
#parent_element.find_element_by_css_selector(span.img_out)                    #再定位子元素

3.5颅围、多窗口處理

1伟葫、如何判斷是否為一個窗口?通過獲取窗口的句柄院促,看是否有多個句柄筏养,是否有新的句柄,如果有新的句柄一疯,表示有一個新的窗口

#多窗口的處理#如何判斷是否為新的窗口
#獲取窗口的句柄(一個窗口對應一個句柄撼玄,句柄是指向這個窗口)
handles = driver.window_handles             #返回的是一個列表對象  
print(handles)
結(jié)果:
[
'CDwindow-4482B00295E5D9F37BD79461F3055B2A', 
'CDwindow-DB98CA0F85C348ED9282D2D4F870B3B2'
]

如何跳轉(zhuǎn)到新的窗口

driver.switch_to.window(handles[1])

3.6、鼠標懸停

在做自動化測試的時候墩邀,經(jīng)常會遇到這種情況掌猛,某個頁面元素,你必須要把鼠標移動到上面才能顯示出元素眉睹。那么這種情況荔茬,我們怎么處理呢?竹海,selenium給我們提供了一個類來處理這類事件——ActionChains慕蔚。

ActionChains可以對需要模擬鼠標操作才能進行的情況,比如單擊斋配、雙擊孔飒、點擊鼠標右鍵、拖拽等等進行操作艰争。ActionChains方法列表:

move_to_element(to_element) —— 鼠標移動到某個元素(鼠標懸停)
context_click(on_element=None) —— 點擊鼠標右鍵
double_click(on_element=None) ——雙擊鼠標左鍵
drag_and_drop(source, target) ——拖拽到某個元素然后松開
drag_and_drop_by_offset(source, xoffset, yoffset) ——拖拽到某個坐標然后松開
perform() —— 執(zhí)行鏈中的所有動作
ActionChains(driver).move_to_element(driver.find_element_by_xpath('//*[@id="header"]/div[2]/div/ul/li[3]/a')).perform()                     # 我要借款
# 點擊申請貸款
driver.find_element_by_xpath('//*[@id="header"]/div[2]/div/ul/li[3]/div/a[3]').click() 

3.7坏瞄、內(nèi)嵌網(wǎng)頁的處理(iframe幀的處理)

需要先跳轉(zhuǎn)到內(nèi)嵌網(wǎng)頁中,然后在去定位元素

driver.switch_to.frame(0) #1. frame名字或id  2. frame的索引第幾個 3. 先定位這個frame元素
driver.find_element_by_xpath('/html/body').send_keys('購房借款')   #輸入借款描述
先定位到iframe甩卓,然后跳轉(zhuǎn)
element = driver.find_element_by_xpath('//*[@id="J_save_deal_form"]/div[1]/div[35]/div/div/div[2]/iframe')
    driver.switch_to.frame(element)

跳入iframe處理完成之后鸠匀,一定要跳出iframe

driver.switch_to.parent_frame()     #跳到上一級的iframe中driver.switch_to.default_content()  #跳到最外層

3.8、對話框的處理

JavaScript 有三種彈窗 Alert (只有確定按鈕), Confirmation (確定,取消等按鈕), Prompt (有輸入對話框)逾柿。

1.accept() 相當于點擊彈出框的確定按鈕:driver.switchTo().alert.accept();
2.dismiss() 相當于點擊彈出框的取消按鈕:driver.switchTo().alert.dismiss();
3.SendKeys(String input)針對于prompt情況的輸入:driver.switchTo().alert.sendKeys("可以輸入");
4.getText()獲取彈出框文本內(nèi)容:driver.switchTo().alert.getText();

4缀棍、各種元素定位異常的原因

1、頁面加載延遲導致元素定位失敗

說明:以后注意机错,凡是出現(xiàn)有點擊頁面跳轉(zhuǎn)的情況爬范,小心因為頁面延遲導致元素定位失敗的情況。

解決辦法:

1弱匪、睡覺 強制等待——不好青瀑,如果腳本中出現(xiàn)很多的sleep(1),會導致整個腳本的執(zhí)行效率。

因為:可能頁面在1s之內(nèi)早就已經(jīng)出現(xiàn)了,但是由于用的是sleep(1)狱窘,它會一直等,知道1s結(jié)束财搁。

time.sleep(1)

2蘸炸、隱式等待:

原理:如果在前面配置了隱式等待,那么只要在腳本執(zhí)行過程中尖奔,出現(xiàn)有頁面跳轉(zhuǎn)的情況搭儒,都會延遲等待頁面出現(xiàn),如果頁面一旦出現(xiàn)可以定位到元素了提茁,腳本會繼續(xù)往下執(zhí)行淹禾,而不會等到10s。如果在10s之內(nèi)還定位的元素沒有出現(xiàn)茴扁,那么會報超時異常铃岔。

隱式等待是等頁面加載,而不是元素加載G突稹;傧啊!(隱式等待就是針對頁面的卖丸,顯式等待是針對元素的纺且。)

#1. 實例化一個webdriver對象
driver = webdriver.Chrome()
driver.implicitly_wait(10)              #設定隱式等待10s

3、顯示等待:

用于等待某個元素出現(xiàn)稍浆,然后再繼續(xù)執(zhí)行后續(xù)代碼载碌。顯式等待是等元素加載!P品恪嫁艇!

這里會用到一個類 WebDriverWait類

原理:總共設定時間是10s,在10s鐘之內(nèi),每隔0.5s會去找一次元素为鳄,循環(huán)找裳仆,如果在10s之內(nèi)元素還沒有出現(xiàn),則拋出異常NoSuchElementException孤钦,如果找到了歧斟,則返回找到的這個元素。然后再去執(zhí)行下一步的操作偏形。

element = WebDriverWait(driver,10).until(lambda x: x.find_element_by_xpath('//*[@id="J_save_deal_form"]/div[1]/div[7]/dl/dd/a[2]'))
element.click()#點擊有

2静袖、換其他定位方式,比如css,xpath等

3俊扭、考慮是否有iframe內(nèi)嵌網(wǎng)頁的問題

4队橙、考慮是否有多窗口問題

5、考慮是否因為滾動條位置不對,元素在頁面上看不見

6捐康、考慮是否為動態(tài)ID

5仇矾、自動化工程維護管理與優(yōu)化

1、unittest框架

unittest框架解总,Pyunit框架是一個單元測試框架贮匕,基于Python,Junit,TestNg樣式單元測試框架花枫,它基礎Java刻盐;自動化中利用unittest框架用來管理自動化用例,加載用例劳翰,執(zhí)行用例敦锌。

1.1、Unittest 核心組件
  • test fixture(測試固件)

包含一個Setup()方法/函數(shù)佳簸,tearDown()方法/函數(shù)乙墙,用例執(zhí)行之前都會先執(zhí)行Setup()方法/函數(shù),主要是完成一些準備初始化的工作,比如創(chuàng)建臨時的數(shù)據(jù)庫溺蕉,文件和目錄伶丐,用例數(shù)據(jù)讀取,瀏覽器的打開等疯特,用例執(zhí)行完成之后哗魂,會執(zhí)行tearDown()方法/函數(shù),完成一些清理回收的工作漓雅,比如數(shù)據(jù)庫斷開录别,關(guān)閉瀏覽器

(1)比如說在這個測試用例中需要訪問數(shù)據(jù)庫邻吞,那么可以在setUp()中建立數(shù)據(jù)庫連接以及進行一些初始化组题,在tearDown()中清除在數(shù)據(jù)庫中產(chǎn)生的數(shù)據(jù),然后關(guān)閉連接抱冷。注意tearDown的過程很重要崔列,要為以后的TestCase留下一個干凈的環(huán)境。

  • test case(測試用例):

什么是測試用例呢旺遮?就是一個完整的測試流程,包括測試前準備環(huán)境的搭建(setUp)赵讯,以及測試后環(huán)境的還原(tearDown),還有包括用例方法耿眉,每個用例方法都必須要以test開頭

  • test suite(測試套件):

多個測試用例的集合就是 suite边翼,一個 suite 可以包含多個 測試用例,也可以嵌套 suite鸣剪∽榈祝可以通過addTest()方法手動增加Test Case丈积,也可通過TestLoader自動添加Test Case,TestLoader在添加用例時债鸡,會沒有順序江滨。

  • test runner(運行器):

用來執(zhí)行測試套件中測試用例的,最終執(zhí)行完成之后會生成一個測試結(jié)果厌均。

  • TestLoader(加載器):用來加載用例牙寞,把用例加載到測試套件中
  • Test Result(測試結(jié)果):包括運行了多少測試用例,成功了多少莫秆,失敗了多少等信息。
1.2悔详、執(zhí)行原理
img
1.3镊屎、unittest常用的斷言方法
assertEqual(a, b)   判斷a==b
assertNotEqual(a, b)    判斷a!=b
assertTrue(x)   bool(x) is True
assertFalse(x)  bool(x) is False
assertIs(a, b)  a is b
assertIsNot(a, b)   a is not b
assertIsNone(x)     x is None
assertIsNotNone(x)  x is not None
assertIn(a, b)  a in b
assertNotIn(a, b)   a not in b
assertIsInstance(a, b)  isinstance(a, b)                #判斷對象的茄螃,對象相對判斷
assertNotIsInstance(a, b)   not isinstance(a, b)        #對象不相等的判斷

1.3缝驳、如何使用unittest框架寫用例:

1、導包

import unittest

2归苍、定義一個類用狱,繼承unittest.TestCase基類

class FwLogin(unittest.TestCase):

3、重寫/覆蓋TestCase中的setUp()拼弃,tearDown()方法夏伊,在這個兩個方法中去搭建測試用例的環(huán)境,清除恢復數(shù)據(jù)

class FwLogin(unittest.TestCase):
    def setUp(self):
        '''
        用例執(zhí)行之前都會先執(zhí)行Setup()方法/函數(shù),主要是完成一些準備初始化的工作
        :return:
        '''
        self.driver = webdriver.Chrome()
        self.driver.maximize_window()
        self.driver.implicitly_wait(10)
        self.driver.get('http://localhost/fw')

    def tearDown(self):
        '''
        用例執(zhí)行完成之后吻氧,會執(zhí)行**tearDown()**方法/函數(shù)溺忧,完成一些銷毀的工作。
        :return:
        '''
        self.driver.quit()

4盯孙、實現(xiàn)用例方法鲁森,每個用例方法必須以test開頭

class FwLogin(unittest.TestCase):
    def setUp(self):
        '''
        用例執(zhí)行之前都會先執(zhí)行Setup()方法/函數(shù),主要是完成一些準備初始化的工作
        :return:
        '''
        self.driver = webdriver.Chrome()
        self.driver.maximize_window()
        self.driver.implicitly_wait(10)
        self.driver.get('http://localhost/fw')

    def test_fw_normal_login(self):
        '''
        用例方法
        :return:
        '''
        #1. 定位用戶名輸入框,輸入用戶名
        self.driver.find_element_by_xpath('//*[@id="login-email-address"]').send_keys('jason')

        #2. 定位密碼輸入框振惰,輸入密碼
        self.driver.find_element_by_id('login-password').send_keys('zgp123456')

        #3. 定位登錄按鈕歌溉,點擊登錄按鈕
        self.driver.find_element_by_id('Iajax-login-submit').click()

        #4. 定位取消按鈕,點擊取消按鈕
        self.driver.find_element_by_xpath('//*[@id="fanwe_msg_box"]/table/tbody/tr/td[2]/div[3]/input[2]').click()

        #5. 斷言
        # 4. 斷言骑晶,檢查實際結(jié)果與預期結(jié)果是否一致
        # 4.1 考慮頁面跳轉(zhuǎn)是否正常痛垛,a. 檢查頁面title  b. 檢查頁面url  頁面如果沒有發(fā)生跳轉(zhuǎn),檢查頁面上有哪些新變化透罢,去檢查核心信息
        page_element_info = self.driver.find_element_by_xpath(
            '/html/body/div[2]/div/div[2]/div[1]/div/div[1]/span/span').text

        try:
            assert page_element_info == 'jason', '頁面信息校驗失敯窕蕖!'
            print('方維登錄-正常登錄用例羽圃,測試通過乾胶!')
        except AssertionError as e:
            print('方維登錄-正常登錄用例抖剿,測試不通過! %s' % e)

    def tearDown(self):
        '''
        用例執(zhí)行完成之后识窿,會執(zhí)行**tearDown()**方法/函數(shù)斩郎,完成一些銷毀的工作。
        :return:
        '''
        self.driver.quit()

5喻频、利用TestLoader加載用例到測試套件中

import unittest

from test_case.fw_login import FwLogin
from test_case.fw_register import FwRegister

if __name__ == '__main__':
    #加載用例
    #方法1:
    #1. 創(chuàng)建一個測試套件
    suite = unittest.TestSuite()

    #2. 加載用例到測試套件中
    suite.addTest(FwLogin('test_fw_normal_login'))
    suite.addTest(FwLogin('test_fw_empty_user_login'))
    suite.addTest(FwRegister('test_fw_normal_register'))

    #3. 創(chuàng)建一個執(zhí)行器
    runner = unittest.TextTestRunner()

    #4. 執(zhí)行用例
    runner.run(suite)
#方法2:
# 測試套件
suite = unittest.TestSuite()
# 測試用例加載器
loader = unittest.TestLoader()
# 把測試用例加載到測試套件中
suite.addTests(loader.loadTestsFromTestCase(FwLogin))
suite.addTests(loader.loadTestsFromTestCase(FwRegister))

#3. 創(chuàng)建一個執(zhí)行器
runner = unittest.TextTestRunner()

#4. 執(zhí)行用例
runner.run(suite)
#方法3:
suite = unittest.defaultTestLoader.discover('D:\\selenium\\1947_Web_Fw_Project\\test_case\\',pattern='fw*.py')
#3. 創(chuàng)建一個執(zhí)行器
runner = unittest.TextTestRunner()

#4. 執(zhí)行用例
runner.run(suite)

6缩宜、利用TextTestRunner執(zhí)行器去執(zhí)行測試套件中的用例

#3. 創(chuàng)建一個執(zhí)行器
runner = unittest.TextTestRunner()
#4. 執(zhí)行用例
runner.run(suite)
#1. 導包
import unittest
from selenium import webdriver

#2. 定義一個類,繼承unittest.TestCase
#只有繼承了unittest.TestCase才成為了用例甥温,不繼承只是一個普通的類
class FwLogin(unittest.TestCase):
    def setUp(self):
        '''
        用例執(zhí)行之前都會先執(zhí)行Setup()方法/函數(shù),主要是完成一些準備初始化的工作
        :return:
        '''
        self.driver = webdriver.Chrome()
        self.driver.maximize_window()
        self.driver.implicitly_wait(10)
        self.driver.get('http://localhost/fw')

    def test_fw_normal_login(self):
        '''
        用例方法
        :return:
        '''
        #1. 定位用戶名輸入框锻煌,輸入用戶名
        self.driver.find_element_by_xpath('//*[@id="login-email-address"]').send_keys('jason')

        #2. 定位密碼輸入框,輸入密碼
        self.driver.find_element_by_id('login-password').send_keys('zgp123456')

        #3. 定位登錄按鈕姻蚓,點擊登錄按鈕
        self.driver.find_element_by_id('Iajax-login-submit').click()

        #4. 定位取消按鈕宋梧,點擊取消按鈕
        self.driver.find_element_by_xpath('//*[@id="fanwe_msg_box"]/table/tbody/tr/td[2]/div[3]/input[2]').click()

        #5. 斷言
        # 4. 斷言,檢查實際結(jié)果與預期結(jié)果是否一致
        # 4.1 考慮頁面跳轉(zhuǎn)是否正常狰挡,a. 檢查頁面title  b. 檢查頁面url  頁面如果沒有發(fā)生跳轉(zhuǎn)捂龄,檢查頁面上有哪些新變化,去檢查核心信息
        page_element_info = self.driver.find_element_by_xpath(
            '/html/body/div[2]/div/div[2]/div[1]/div/div[1]/span/span').text

        try:
            assert page_element_info == 'jason', '頁面信息校驗失敿尤倦沧!'
            print('方維登錄-正常登錄用例,測試通過它匕!')
        except AssertionError as e:
            print('方維登錄-正常登錄用例展融,測試不通過! %s' % e)

    def test_fw_empty_user_login(self):
        # 1. 定位用戶名輸入框豫柬,清空用戶名文本框
        self.driver.find_element_by_xpath('//*[@id="login-email-address"]').clear()

        # 2. 定位密碼輸入框愈污,輸入密碼
        self.driver.find_element_by_id('login-password').send_keys('zgp123456')

        # 3. 定位登錄按鈕,點擊登錄按鈕
        self.driver.find_element_by_id('Iajax-login-submit').click()

        # 5. 斷言
        assert_text = self.driver.find_element_by_xpath('//*[@id="fanwe_error_box"]/table/tbody/tr/td[2]/div[2]').text
        try:
            self.assertEqual('Email格式錯誤轮傍,請重新輸入或者昵稱格式錯誤暂雹,請重新輸入',assert_text,'信息不一致!')
            print('方維登錄-用戶名為空用例執(zhí)行成功创夜!')
        except AssertionError as e:
            print('方維登錄-用戶名為空用例執(zhí)行失敽脊颉! %s' %e)

    def tearDown(self):
        '''
        用例執(zhí)行完成之后驰吓,會執(zhí)行**tearDown()**方法/函數(shù)涧尿,完成一些銷毀的工作。
        :return:
        '''
        self.driver.quit()

if __name__=='__main__':
    unittest.main()

2檬贰、數(shù)據(jù)驅(qū)動姑廉,參數(shù)化

用戶數(shù)據(jù)提取到Excel表格中,進行統(tǒng)一化管理翁涤,比如:就拿登錄模塊為例桥言,那用戶數(shù)據(jù)就是'用戶名'萌踱,‘密碼’,‘斷言文本’ 号阿,把這些數(shù)據(jù)提取到Excel表格中并鸵,實現(xiàn)數(shù)據(jù)與腳本的分離,然后封裝一個讀取Excel文件數(shù)據(jù)的函數(shù)實現(xiàn)數(shù)據(jù)的讀取扔涧,并利用DDT(Data Driver Test)模型講數(shù)據(jù)引用到腳本中去實現(xiàn)參數(shù)化园担。

對于Excel表格數(shù)據(jù)的讀取我們都是調(diào)用公司封裝好的模塊中的API函數(shù)來實現(xiàn)的。

對于Excel表格數(shù)據(jù)的讀取需要封裝一個模塊實現(xiàn)數(shù)據(jù)的讀取枯夜,這里需要用到xlrd,xlwt兩個庫弯汰,通過調(diào)用xlrd庫中的API來實現(xiàn)對數(shù)據(jù)的讀取,具體代碼如下:

# coding:utf-8
import xlrd
class ExcelUtil():

    def __init__(self, excelPath, sheetName):
        '''
        構(gòu)造方法
        :param excelPath:
        :param sheetName:
        '''
        self.data = xlrd.open_workbook(excelPath)
        self.table = self.data.sheet_by_name(sheetName)
        # 獲取第一行作為key值
        self.keys = self.table.row_values(0)
        # 獲取總行數(shù)
        self.rowNum = self.table.nrows
        # 獲取總列數(shù)
        self.colNum = self.table.ncols

    def dict_data(self):
        if self.rowNum <= 1:
            print("總行數(shù)小于1")
        else:
            r = []
            j=1
            for i in range(self.rowNum-1):
                s = {}
                # 從第二行取對應values值
                values = self.table.row_values(j)
                for x in range(self.colNum):
                    s[self.keys[x]] = values[x]
                r.append(s)
                j+=1
            return r

if __name__ == "__main__":
    # filepath = "D:\\test\\web-project\\5ke\\testdata.xlsx"
    filepath = "D:\\selenium\\1947_Web_Fw_Project\\data\\測試數(shù)據(jù).xls"
    sheetName = "登錄"
    data = ExcelUtil(filepath, sheetName)
    print(data.dict_data())

引入DDT(Data Driver Test)模型湖雹,利用DDT來實現(xiàn)數(shù)據(jù)的驅(qū)動

1.安裝ddt

pip install ddt

2.導入ddt

3.在用例類上引用ddt

@ddt.ddt
class FwLogin(unittest.TestCase):

4.在測試用例函數(shù)上去引用測試數(shù)據(jù)蝙泼,這樣在測試用例函數(shù)里面就可以使用引用過來的用戶數(shù)據(jù)了。

@ddt.data(*testdata)
def test_fw_normal_login(self,data):

完整的代碼:

#1. 導包
import unittest
from selenium import webdriver
from common.readExcel import ExcelUtil
import ddt

#1. 讀取excel表格的測試數(shù)據(jù)
testdata = ExcelUtil("D:\\selenium\\1947_Web_Fw_Project\\data\\測試數(shù)據(jù).xls",'登錄').dict_data()
#[{'case_name':'正常登錄','username':'jason'劝枣,'password':'zgp123456','assert_text':'jason'},{},{},{},{}]

#2. 定義一個類,繼承unittest.TestCase
#只有繼承了unittest.TestCase才成為了用例织鲸,不繼承只是一個普通的類
@ddt.ddt
class FwLogin(unittest.TestCase):
    def setUp(self):
        '''
        用例執(zhí)行之前都會先執(zhí)行Setup()方法/函數(shù),主要是完成一些準備初始化的工作
        :return:
        '''
        self.driver = webdriver.Chrome()
        self.driver.maximize_window()
        self.driver.implicitly_wait(10)
        self.driver.get('http://localhost/fw')

    @ddt.data(*testdata)
    def test_fw_normal_login(self,data):

        '''
        用例方法
        :return:
        '''
        # print(data)
        #1. 定位用戶名輸入框舔腾,輸入用戶名
        self.driver.find_element_by_xpath('//*[@id="login-email-address"]').send_keys(data['username'])

        #2. 定位密碼輸入框,輸入密碼
        self.driver.find_element_by_id('login-password').send_keys(data['password'])

        #3. 定位登錄按鈕搂擦,點擊登錄按鈕
        self.driver.find_element_by_id('Iajax-login-submit').click()

        if (data['case_name']=='正常登錄'):
            #4. 定位取消按鈕稳诚,點擊取消按鈕
            self.driver.find_element_by_xpath('//*[@id="fanwe_msg_box"]/table/tbody/tr/td[2]/div[3]/input[2]').click()

        #5. 斷言
        # 4. 斷言,檢查實際結(jié)果與預期結(jié)果是否一致
        # 4.1 考慮頁面跳轉(zhuǎn)是否正常瀑踢,a. 檢查頁面title  b. 檢查頁面url  頁面如果沒有發(fā)生跳轉(zhuǎn)扳还,檢查頁面上有哪些新變化,去檢查核心信息
        if (data['case_name']=='正常登錄'):
            page_element_info = self.driver.find_element_by_xpath(
                '/html/body/div[2]/div/div[2]/div[1]/div/div[1]/span/span').text
            #print(data['assert_text'])
            #print(page_element_info)
            #try:
            assert page_element_info == data['assert_text'], '頁面信息校驗失敵髫病氨距!'
        else:
            assert_info = self.driver.find_element_by_xpath('//*[@id="fanwe_error_box"]/table/tbody/tr/td[2]/div[2]').text
            self.assertEqual(assert_info,data['assert_text'],'信息不一致,斷言錯誤棘劣!')

    def tearDown(self):
        '''
        用例執(zhí)行完成之后俏让,會執(zhí)行**tearDown()**方法/函數(shù),完成一些銷毀的工作茬暇。
        :return:
        '''
        self.driver.quit()

if __name__=='__main__':
    unittest.main()

3首昔、模塊的封裝

主要是針對一些核心的常用的功能業(yè)務模塊盡心封裝,方便調(diào)用糙俗,比如:登錄模塊勒奇,打開瀏覽器的操作,針對元素定位做二次封裝處理巧骚,數(shù)據(jù)庫的操作的封裝赊颠,Excel表格數(shù)據(jù)的讀取的封裝等等

Excel表格操作的封裝:

# coding:utf-8
import xlrd
class ExcelUtil():

    def __init__(self, excelPath, sheetName):
        '''
        構(gòu)造方法
        :param excelPath:
        :param sheetName:
        '''
        self.data = xlrd.open_workbook(excelPath)
        self.table = self.data.sheet_by_name(sheetName)
        # 獲取第一行作為key值
        self.keys = self.table.row_values(0)
        # 獲取總行數(shù)
        self.rowNum = self.table.nrows
        # 獲取總列數(shù)
        self.colNum = self.table.ncols

    def dict_data(self):
        if self.rowNum <= 1:
            print("總行數(shù)小于1")
        else:
            r = []
            j=1
            for i in range(self.rowNum-1):
                s = {}
                # 從第二行取對應values值
                values = self.table.row_values(j)
                for x in range(self.colNum):
                    s[self.keys[x]] = values[x]
                r.append(s)
                j+=1
            return r

if __name__ == "__main__":
    # filepath = "D:\\test\\web-project\\5ke\\testdata.xlsx"
    filepath = "D:\\selenium\\1947_Web_Fw_Project\\data\\測試數(shù)據(jù).xls"
    sheetName = "登錄"
    data = ExcelUtil(filepath, sheetName)
    print(data.dict_data())

數(shù)據(jù)庫的封裝:

'''
封裝:
    1. 連接數(shù)據(jù)庫
    2. 增格二,刪,查巨税,改的操作蟋定。
'''
import pymysql.cursors

def connect_db(host,username,pwd,db_name,charset='utf8'):
    '''
    功能:連接數(shù)據(jù)庫
    :param host:            主機ip
    :param username:        賬號
    :param pwd:             密碼
    :param db_name:         數(shù)據(jù)庫名字
    :param charset:         編碼方式
    :return:
    '''
    try:
        con = pymysql.connect(
            host=host,              # 數(shù)據(jù)庫服務器的ip地址
            user=username,          # 數(shù)據(jù)庫賬號
            password=pwd,           # 數(shù)據(jù)庫密碼
            db=db_name,             # 數(shù)據(jù)庫名稱
            charset=charset,        # 編碼方式
            cursorclass=pymysql.cursors.DictCursor
        )
    except pymysql.err.Error as e:                                      #捕獲異常
        print("數(shù)據(jù)庫連接失敗:%s" %e)
    return con  # 連接對象返回出去

def close_db(connect):
    '''
    功能:關(guān)閉數(shù)據(jù)庫的連接
    :param connect:
    :return:
    '''
    connect.close()

def select(connect,sql,params=None):
    '''
    功能:查詢操作
    :param connect:
    :param sql:
    :param params:
    :return:
    '''
    try:
        #1. 創(chuàng)建游標
        crs = connect.cursor()
        #2. 執(zhí)行sql語句
        crs.execute(sql,params)                 #執(zhí)行sql語句可能會失敗
        #3. 提交
        connect.commit()
        #4. 提取數(shù)據(jù)
        result = crs.fetchall()
        return result                           #正常返回 查詢的數(shù)據(jù)
    except pymysql.err.Error as e:
        print('執(zhí)行查詢操作失敗 %s' %e)
        return False                            #如果出現(xiàn)異常,返回false
    finally:                                    #finally里面的語句一定會執(zhí)行草添。
        #5. 關(guān)游標
        crs.close()

def insert(connect,sql,params):
    try:
        # 1. 創(chuàng)建游標
        crs = connect.cursor()
        # 2. 執(zhí)行sql語句
        crs.execute(sql, params)
        # 3. 提交
        connect.commit()
        return True
    except pymysql.err.Error as e:
        print('執(zhí)行增加數(shù)據(jù)操作失敗 %s' %e)
        return False
    finally:
        # 5. 關(guān)游標
        crs.close()

def delete(connect,sql,params):
    try:
        # 1. 創(chuàng)建游標
        crs = connect.cursor()
        # 2. 執(zhí)行sql語句
        crs.execute(sql, params)
        # 3. 提交
        connect.commit()
        return True
    except pymysql.err.Error as e:
        print('執(zhí)行刪除數(shù)據(jù)操作失敗 %s' % e)
        return False
    finally:
        # 5. 關(guān)游標
        crs.close()

def update(connect,sql,params):
    try:
        # 1. 創(chuàng)建游標
        crs = connect.cursor()
        # 2. 執(zhí)行sql語句
        crs.execute(sql, params)
        # 3. 提交
        connect.commit()
        return True
    except pymysql.err.Error as e:
        print('執(zhí)行修改數(shù)據(jù)操作失敗 %s' % e)
        return False
    finally:
        # 5. 關(guān)游標
        crs.close()

打開瀏覽器的封裝:

def _open_browser(url,mode):
    '''
    功能:打開瀏覽器驶兜,加載頁面
    :param url:
    :param mode:
    :return:
    '''
    if mode in ['Chrome','chrome']:
        driver = webdriver.Chrome()                 #打開瀏覽器
    elif mode in ['Firefox','ff','firefox']:
        driver = webdriver.Firefox()
    elif mode in ['Ie','ie']:
        driver = webdriver.Ie()

    driver.implicitly_wait(10)                      #設置隱式等待延遲10s
    driver.maximize_window()                        #窗口最大化

    driver.get(url)                                 #加載頁面
    return driver

登錄模塊的封裝:

#1. 登錄
def _login(driver,username,password):
    # 3.1 定位用戶名輸入框,輸入用戶名
    driver.find_element_by_id('login-email-address').send_keys(username)

    # 3.2 定位密碼輸入框远寸,輸入密碼
    driver.find_element_by_id('login-password').send_keys(password)

    # 3.3 定位登錄按鈕抄淑,點擊登錄按鈕
    driver.find_element_by_id('Iajax-login-submit').click()

    # 3.4 定位取消按鈕,點擊取消按鈕
    driver.find_element_by_xpath('//*[@id="fanwe_msg_box"]/table/tbody/tr/td[2]/div[3]/input[2]').click()

4驰后、自動化測試報告

HTMLReport是一個單元測試測試運行器肆资,可以將測試結(jié)果保存在 Html 文件中,用于人性化的結(jié)果顯示灶芝。
僅支持Python 3.x

1郑原、導包
import HTMLReport
suite = unittest.defaultTestLoader.discover('D:\\selenium\\1947_Web_Fw_Project\\test_case\\',pattern='fw*.py')

#3. 創(chuàng)建一個執(zhí)行器
# runner = unittest.TextTestRunner()
runner = HTMLReport.TestRunner(
                    #report_file_name='test',  # 報告文件名,如果未賦值夜涕,將采用“test+時間戳”
                    output_path='report',  # 保存文件夾名犯犁,默認“report”
                    title='測試報告',  # 報告標題,默認“測試報告”
                    description='無測試描述',  # 報告描述女器,默認“測試描述”
                    thread_count=1,  # 并發(fā)線程數(shù)量(無序執(zhí)行測試)酸役,默認數(shù)量 1
                    thread_start_wait=3,  # 各線程啟動延遲,默認 0 s
                    sequential_execution=False,  # 是否按照套件添加(addTests)順序執(zhí)行驾胆,
                    # 會等待一個addTests執(zhí)行完成涣澡,再執(zhí)行下一個,默認 False
                    # 如果用例中存在 tearDownClass 丧诺,建議設置為True入桂,
                    # 否則 tearDownClass 將會在所有用例線程執(zhí)行完后才會執(zhí)行。
                    # lang='en'
                    lang='cn'  # 支持中文與英文驳阎,默認中文
                )
#4. 執(zhí)行用例
runner.run(suite)
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末事格,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子搞隐,更是在濱河造成了極大的恐慌驹愚,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件劣纲,死亡現(xiàn)場離奇詭異逢捺,居然都是意外死亡,警方通過查閱死者的電腦和手機癞季,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進店門劫瞳,熙熙樓的掌柜王于貴愁眉苦臉地迎上來倘潜,“玉大人,你說我怎么就攤上這事志于′桃颍” “怎么了?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵伺绽,是天一觀的道長养泡。 經(jīng)常有香客問我,道長奈应,這世上最難降的妖魔是什么澜掩? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮杖挣,結(jié)果婚禮上肩榕,老公的妹妹穿的比我還像新娘。我一直安慰自己惩妇,他們只是感情好株汉,可當我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著歌殃,像睡著了一般抹竹。 火紅的嫁衣襯著肌膚如雪柱彻。 梳的紋絲不亂的頭發(fā)上哨毁,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天窘俺,我揣著相機與錄音猿涨,去河邊找鬼办龄。 笑死醇王,一個胖子當著我的面吹牛猎贴,可吹牛的內(nèi)容都是我干的朵你。 我是一名探鬼主播各聘,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼抡医!你這毒婦竟也來了躲因?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤忌傻,失蹤者是張志新(化名)和其女友劉穎大脉,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體水孩,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡镰矿,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了俘种。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片秤标。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡绝淡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出苍姜,到底是詐尸還是另有隱情牢酵,我是刑警寧澤,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布衙猪,位于F島的核電站馍乙,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏屈嗤。R本人自食惡果不足惜潘拨,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望饶号。 院中可真熱鬧铁追,春花似錦、人聲如沸茫船。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽算谈。三九已至涩禀,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間然眼,已是汗流浹背艾船。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留高每,地道東北人屿岂。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像鲸匿,于是被迫代替她去往敵國和親爷怀。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,577評論 2 353

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