精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

iOS單元測試和UI測試全面解析

譯文
移動開發 iOS
當今世界,測試成為軟件工程開發的必需環節。高質量的單元測試與UI測試可以確保您的軟件無后顧之憂,特別是把一些高質量函數或者模塊納入您的代碼倉庫中重用之時。在本篇中,我們將向您全面細致地介紹基于iOS平臺進行單元測試和UI測試的完整過程及相關技巧。

【51CTO.com快譯】編寫測試可不是一項迷人的工作;然而,由于測試可以避免使你的寶貝應用程序變成一塊充斥錯誤的大垃圾場,所以編寫測試又是一項非常有必要做的工作。如果你正在閱讀本文,那么你應當已經知道你應該為您的代碼和用戶界面編寫測試,只是不確定如何在Xcode中編寫測試。

[[186816]]

也許你已經開發出一個能夠工作的應用程序,只是還沒有對它進行測試;另一方面,當您擴展該應用程序時,你又想對其任何的更改進行測試。也許你已經寫了一些測試,但尚不能確定它們是否是正確的測試。或者,你現在正在開發您的應用程序,并且想隨著工作的進展對之進行測試。

本教程將向您全面展示如何使用Xcode中的測試導航器來測試應用程序的模型和異步方法,以及如何通過使用代理(注stub,有的文章譯作“存根”)和模擬(mock)來模仿與庫或系統對象的交互,如何測試用戶界面和性能,以及如何使用代碼覆蓋工具。隨著文章的展開,你會不斷熟悉一些與測試相關的術語,到文章結尾時你會沉著地把依賴關系注入到你的被測系統(SUT,system under test)中!

測試,測試……

測試什么?

在寫任何測試之前,首先要明確最基本的問題︰你需要測試什么?如果你的目標是擴展一款現有的應用程序,那么您應該首先為您計劃更改的任何組件編寫測試。

更一般的情況下,你的測試應包括如下一些內容︰

  • 核心功能︰模型類和方法及其與控制器的交互
  • 最常見的用戶界面工作流
  • 邊界條件
  • 錯誤修復

當務之急

首字母縮略詞FIRST描述了一套簡明有效的單元測試標準。這些標準是︰

  • Fast(快速)︰測試的運行速度應該很快,這樣一來人們就不會介意運行它們。
  • Independent/Isolated(獨立/分離)︰一個測試不應因另一個測試而進行安裝或拆卸。
  • Repeatable(可重復)︰每次運行測試時,您應該獲得相同的結果。值得注意的是,外部數據提供者和并發問題可能會導致程序的間歇性故障。
  • Self-validating(自我驗證)︰測試應該能夠完全自動化進行;輸出應該要么是“pass”(即“通過”)要么是“fail”(即“失敗”),而不是提供給程序員一個解釋性的日志文件。
  • Timely(及時)︰理想情況下,應該只是在你編寫生產代碼之前編寫測試。

遵循上述FIRST原則進行測試能夠確保您的測試明確而有用,而不致使之成為您的應用程序中的路障。

開始

首先,請從網址https://koenig-media.raywenderlich.com/uploads/2016/12/Starters.zip處下載、解壓縮、打開并觀察本文提供的兩個初始示例工程BullsEye和HalfTunes。

注意,工程BullsEye基于文章https://www.raywenderlich.com/store/ios-apprentice中提供的一個樣本程序。我已經把游戲邏輯提取到一個BullsEyeGame類中,并相應地添加了另一種游戲風格。

在游戲的右下角提供了一個分段的控制器組件,供用戶選擇游戲風格︰或者是Slide類型,允許玩家移動滑塊組件以盡可能接近目標值;或者是Type類型,允許玩家猜測滑塊到達的位置。控件相應的動作代碼中還會將用戶選擇的游戲風格存儲為該用戶的默認設置。

另一個示例工程HalfTunes則來自于我們的另一個教程NSURLSession(https://www.raywenderlich.com/110458/nsurlsession-tutorial-getting-started),現已被更新到Swift 3版本。用戶可以使用iTunes API查詢歌曲,然后下載并播放對應的歌曲片段。

下面,讓我們正式開始測試!

Xcode中的單元測試

創建單元測試目標

Xcode中的測試導航器(Test Navigator)為進行程序測試提供了最容易使用的方式;你可以使用它創建測試目標并在你的程序上運行測試。

現在,請打開工程BullsEye并按下組合鍵Command+5來打開它的測試導航器。

然后,點擊左下方的+按鈕;之后,從菜單中選擇“New Unit Test Target…”命令,如圖所示。

在此,請直接使用默認的名稱BullsEyeTests。當測試包出現在測試導航器中時,單擊它,從而在編輯器中打開它。如果BullsEyeTests不會自動出現,你可以單擊其他導航器,然后再返回到當前測試導航器即可。

注意到,模板導入了XCTest并定義了XCTestCase的一個子類BullsEyeTests,同時提供了setup()方法,tearDown()方法,還有系統默認的示例測試方法。

歸納起來,共有三種辦法可以運行測試類:

1. 使用命令Product\Test或者Command-U;這將會運行所有的測試類。

2. 使用測試導航器中的箭頭命令。

3. 也可以點擊代碼左邊緣上的鉆石按鈕。

另外,您還可以通過單擊測試導航器中或代碼左邊緣上的鉆石按鈕運行單個測試方法。

建議你嘗試上面不同的方式來運行測試,從而感受一下需要多長時間以及運行測試看起來的樣子。當前的樣本測試并不做任何事,所以它們的運行速度會非常快!

當所有測試都成功時,鉆石按鈕會變綠,并在上面顯示對號標記。你可以單擊testPerformanceExample()方法最后面的灰色鉆石按鈕來打開性能結果(Performance Result)小窗進行觀察,參考下圖。

現在,我們并不需要函數testPerformanceExample();所以,把它刪除即可。

使用XCTAssert測試模型

首先,您將使用XCTAssert來測試BullsEye模型的一個核心功能︰一個BullsEyeGame對象能否正確計算出一個回合的得分?

為此,請在文件BullsEyeTests.swift中緊貼著導入語句下方添加下面這一行代碼︰

  1. @testable import BullsEye 

這一行代碼使單元測試能夠訪問到BullsEye中的類和方法。

接下來,請在BullsEyeTests類的頂部添加下面的屬性:

  1. var gameUnderTest: BullsEyeGame! 

然后,在setup()方法中在調用超類語句的下面啟動一個新的BullsEyeGame對象:

  1. gameUnderTest = BullsEyeGame() 
  2.  
  3. gameUnderTest.startNewGame() 

 

上面的代碼將創建一個類級的SUT(System Under Test,測試系統)對象。這樣一來,測試類中的所有測試都可以訪問該SUT對象的屬性和方法。

在這里,你還可以調用游戲的startNewGame方法——此方法只創建一個targetValue值。您的很多測試都將使用這個targetValue值,來測試程序能夠正確計算出游戲中的得分。

最后,切記在tearDown()方法中在調用超類前釋放掉你的SUT對象︰

  1. gameUnderTest = nil 

【注意】一種值得推薦的測試做法是在方法setup()中創建SUT對象并在tearDown()方法中釋放它,以確保每個測試都對應一個徹底的清理。更多的有關細節討論,請參考Jon Reid的帖子http://qualitycoding.org/teardown/

現在,你已經準備好編寫你的第一個測試了!

請使用如下代碼替換工程中的方法testExample():

  1. // XCTAssert to test model 
  2. func testScoreIsComputed() { 
  3.   // 1. given 
  4.   let guess = gameUnderTest.targetValue + 5 
  5.   
  6.   // 2. when 
  7.   _ = gameUnderTest.check(guess: guess) 
  8.   
  9.   // 3. then 
  10.   XCTAssertEqual(gameUnderTest.scoreRound, 95, "Score computed from guess is wrong"

 

測試方法的名稱總是以test開頭,后面跟著的是對它要測試的內容的說明。

一個推薦的做法是把測試方法格式化成given、when和then等幾部分︰

1. 在given部分中,設置所需的任何值。在此示例中,您創建一個猜測值,以便可以指定它與targetValue值區別多大。

2. 在when部分中,執行被測試代碼——調用方法gameUnderTest.check(_:)。

3. 在then部分中,斷言你期望的結果(在現在情況下,gameUnderTest.scoreRound的值是100-5):如果測試失敗則打印對應的消息。

現在,你可以單擊測試導航器或者代碼左邊的鉆石圖標按鈕運行測試。你會注意到應用程序將進行構建并運行起來,最后鉆石圖標將更改為一個綠色的對號標記!

【注意】若要查看XCTestAssertions的完整列表,你可以在按下Command鍵的同時單擊代碼中的XCTAssertEqual打開文件XCTestAssertions.h。此外,你還可以參考蘋果官方網站提供的按類別提供的斷言列表

(https://developer.apple.com/library/prerelease/content/documentation/DeveloperTools/Conceptual/testing_with_xcode/chapters/04-writing_tests.html#//apple_ref/doc/uid/TP40014132-CH4-SW35)。

[[186817]]

另外,上述測試中的Given-When-Then結構來源于行為驅動測試(Behavior Driven Development,簡稱BDD)中的易于理解的行業術語。其實,你還可以使用另外一些命名系統,例如Arrange-Act-Assert和Assemble-Activate-Assert,等等。

調試一個測試

在BullsEyeGame工程中,我故意放置了一個錯誤。現在,我們進行測試,以便找到這個錯誤。為了觀察此錯誤導致的問題,請把testScoreIsComputed重新命名為testScoreIsComputedWhenGuessGTTarget,然后復制、粘貼并編輯它,從而創建另一個方法testScoreIsComputedWhenGuessLTTarget。

在該測試中,在given部分把targetValue減去5,其他保持不變。詳見下列代碼:

  1. func testScoreIsComputedWhenGuessLTTarget() { 
  2.   // 1. given 
  3.   let guess = gameUnderTest.targetValue - 5 
  4.   
  5.   // 2. when 
  6.   _ = gameUnderTest.check(guess: guess) 
  7.   
  8.   // 3. then 
  9.   XCTAssertEqual(gameUnderTest.scoreRound, 95, "Score computed from guess is wrong"

 

注意到:猜測值和targetValue值之間的區別仍然是5,因此分數應仍為95。

在斷點導航器中,添加一個測試失敗(Test Failure)斷點;當一個測試方法發出一個失敗的斷言時這將停止測試運行。

現在運行你的測試:它應該在XCTAssertEqual一行停止,并出示一個測試錯誤。

然后,你可以在調試控制臺上觀察gameUnderTest和guess的輸出結果:

你應該注意到:guess的值是-5,但scoreRound的值是105,而不是95!

為了進一步分析,你可以使用通常的調試過程︰在when語句上設置一個斷點,也在BullsEyeGame.swift文件上設置一個斷點——即在其中的方法check(_:)上設置。然后,再次運行測試,并以逐過程調試方式(即step-over)調試let語句來檢查應用程序中的不同值。

現在的問題是,差值是一個負數;所以,得分是100-(-5)。解決方法是使用差異的絕對值即可。為此,在方法check(_:)中取消正確代碼前面的注釋,并刪除不正確的代碼即可。

刪除上面設置的兩個斷點并再一次運行測試,以確認上面代碼行現在已順利通過。

使用XCTestExpectation測試異步操作

到目前為止,你已經學會了如何測試模型和調試測試失敗。接下來,讓我們繼續學習如何使用XCTestExpectation來測試網絡相關的操作。

首先,請打開HalfTunes項目。你會注意到,它使用URLSession來查詢iTunes API和下載歌曲樣本。假設您想修改它,以便使用AlamoFire進行網絡操作。為了查看是否出現任何中斷情況,您應為網絡操作編寫測試,并在更改代碼之前和之后運行它們。

URLSession方法是異步執行的︰它們會馬上返回,但只有運行一段時間后才真正完成。為了測試異步方法,你應使用XCTestExpectation使你的測試等待異步操作完成。

值得注意的是,異步測試通常很慢,所以你應該把它們與你另外的一些運行速度更快的單元測試分開。

從菜單“+”下選擇并運行命令“New Unit Test Target…”,然后把目標命名為HalfTunesSlowTests。然后,在import語句的下面導入HalfTunes程序:

  1. @testable import HalfTunes 

在此類中的所有測試都將使用默認會話把請求發送到蘋果公司的服務器。所以,我們在方法setup()中聲明并創建一個sessionUnderTest對象,然后在方法tearDown()中釋放它:

  1. var sessionUnderTest: URLSession! 
  2. override func setUp() { 
  3.   super.setUp() 
  4.   sessionUnderTest = URLSession(configuration: URLSessionConfiguration.default
  5. override func tearDown() { 
  6.   sessionUnderTest = nil 
  7.   super.tearDown() 

 

接下來,使用TestExample()函數來替換您的異步測試︰

  1. //異步測試時:成功測試很快,失敗測試卻比較慢 
  2. func testValidCallToiTunesGetsHTTPStatusCode200() { 
  3.   // given 
  4.   let url = URL(string: "https://itunes.apple.com/search?media=music&entity=song&term=abba"
  5.   // 1 
  6.   let promise = expectation(description: "Status code: 200"
  7.   
  8.   // when 
  9.   let dataTask = sessionUnderTest.dataTask(with: url!) { data, response, error in 
  10.     // then 
  11.     if let error = error { 
  12.       XCTFail("Error: \(error.localizedDescription)"
  13.       return 
  14.     } else if let statusCode = (response as? HTTPURLResponse)?.statusCode { 
  15.       if statusCode == 200 { 
  16.         // 2 
  17.         promise.fulfill() 
  18.       } else { 
  19.         XCTFail("Status code: \(statusCode)"
  20.       } 
  21.     } 
  22.   } 
  23.   dataTask.resume() 
  24.   // 3 
  25.   waitForExpectations(timeout: 5, handler: nil) 

 

上面這個測試的目的是檢查發送到iTunes的有效的查詢是否能夠返回狀態碼200。顯然,其中大部分代碼與你在上面應用程序中所寫的一樣,只是增加了如下幾行︰

1.expectation(_:)返回一個XCTestExpectation對象;此對象存儲在變量promise中。此對象的其他常用名字是expectation和future。另外,description參數描述了你期望發生的事情。

2.為了匹配description參數,您需要在異步方法的完成處理程序的成功條件閉包中調用promise.fulfill()。

3.waitForExpectations(_:handler:)的作用是保持所有測試在運行中,直到所有的期望得以實現,或者timeout值指定的時間間隔結束——無論兩者哪一種早發生都行。

現在,再來運行該測試。如果你已經連接到互聯網,則當應用程序在模擬器中加載后成功測試大約花費一秒鐘時間。

使測試失敗更快一些

測試失敗會導致不少問題,但它未必花費很多時間。現在,我們來解決如何快速確定是否您的測試失敗的問題。

為了修改一下您的測試,從而導致異步操作時失敗,你只需要從下面的URL中刪除“itunes”一詞后面的s字母即可:

  1. let url = URL(string: "https://itune.apple.com/search?media=music&entity=song&term=abba"

運行上述測試時︰它會失敗,而且此測試會花費所有指定的超時間隔時間!這是因為它的期望是請求成功——正是在這個位置調用了promise.fulfill()方法。既然請求失敗,那么測試僅當在超過指定時限時才結束。

你可以使這個測試失敗更快一些——這只要通過改變它的期望值即可達到︰不是等待請求成功,而只需要等到異步方法的完成處理程序觸發即可。只要應用程序接收到來自服務器端的響應(或者是成功或者是失敗)這種情況就會發生;但是,這的確符合預期結果。然后,您的測試可以檢查請求是否成功。

為了查看這是如何工作的,您要創建一個新的測試。首先,修復此測試——這可以通過撤消上面的url更改操作輕松完成,然后將下面的測試添加到您的類中︰

  1. // Asynchronous test: faster fail 
  2. func testCallToiTunesCompletes() { 
  3.   // given 
  4.   let url = URL(string: "https://itune.apple.com/search?media=music&entity=song&term=abba"
  5.   // 1 
  6.   let promise = expectation(description: "Completion handler invoked"
  7.   var statusCode: Int
  8.   var responseError: Error? 
  9.   
  10.   // when 
  11.   let dataTask = sessionUnderTest.dataTask(with: url!) { data, response, error in 
  12.     statusCode = (response as? HTTPURLResponse)?.statusCode 
  13.     responseError = error 
  14.     // 2 
  15.     promise.fulfill() 
  16.   } 
  17.   dataTask.resume() 
  18.   // 3 
  19.   waitForExpectations(timeout: 5, handler: nil) 
  20.   
  21.   // then 
  22.   XCTAssertNil(responseError) 
  23.   XCTAssertEqual(statusCode, 200) 

 

上面代碼中最關鍵的一點是,只需輸入完成處理程序實現的期望——這需要大約一秒鐘即會發生。如果請求失敗,那么斷言也會失敗。

現在再來運行上面的測試︰它現在大約需要一秒鐘即會失敗;它的失敗是因為請求失敗了,而不是因為測試運行超時。

修復上面的url,然后再一次運行測試,以確認它現在能夠成功通過測試。

偽造對象和交互

異步測試能夠給你信心——你的代碼會為一個異步API提供正確的輸入。你可能也想測試您的代碼能夠正常工作——當它從URLSession接收輸入時,或當它正確更新了UserDefaults或者CloudKit數據庫時。

大多數應用程序都會與系統或庫對象(你不能控制這些對象)進行交互,而與這些對象的交互測試很可能是極其緩慢的,而且不可重復的——這正違反了文章開始時FIRST原則中的兩條。相反,你可以偽造這些交互——通過從代理(stub)中獲取輸入或更新模擬對象(Mock Object)來實現。

當您的代碼依賴于一個系統或庫中的對象時,通過上面偽造的辦法可以創建一個假的對象來實現那一部分功能并把這種偽造注入到您的代碼中。喬恩·里德的依賴性注入技術文章(https://www.objc.io/issues/15-testing/dependency-injection/)中就介紹了好幾種方法來達到這一目的。

[[186818]]

從代理(stub)中偽造輸入

在本節中的測試中,你將要檢查應用程序的updateSearchResults(_:)方法能夠正確解析由會話下載的數據——通過檢查屬性searchResults.count的值是正確的來實現。SUT是視圖控制器;你要使用代理(stub)技術來偽裝一個會話和一些預先下載的數據。

為此,從“+”菜單下選擇命令“New Unit Test Target…”并命名它為HalfTunesFakeTests。然后,在import語句的下面導入HalfTunes程序:

  1. @testable import HalfTunes 

接下來,聲明SUT,并在setup()方法中創建它,且在tearDown()方法中對之進行釋放:

  1. var controllerUnderTest: SearchViewController! 
  2.   
  3. override func setUp() { 
  4.   super.setUp() 
  5.   controllerUnderTest = UIStoryboard(name"Main",  
  6.       bundle: nil).instantiateInitialViewController() as! SearchViewController! 
  7.   
  8. override func tearDown() { 
  9.   controllerUnderTest = nil 
  10.   super.tearDown() 

 

【注】SUT(被測系統)是視圖控制器,因為HalfTunes工程中擁有大量的視圖控制器問題——所有的工作都是在文件searchviewcontroller.swift中完成的。“將網絡代碼移動到單獨的模塊”(詳見文章http://williamboles.me/networking-with-nsoperation-as-your-wingman/)將會減少這一問題,而且也使測試更為容易。

接下來,您將需要一些樣本JSON數據,供您的偽造的會話提供給你的測試使用。只需要做一少部分工作即可;因此,請限制一下您的來自iTunes的下載結果——在URL字符串的后面添加一個限制串&limit=3:

https://itunes.apple.com/search?media=music&entity=song&term=abba&limit=3

復制此URL并把它粘貼到瀏覽器中。這將下載一個名為1.txt或類似的文件。你可以預覽一下它,以便確認這是一個JSON格式的文件,然后重命名它為abbaData.json,并把該文件添加到HalfTunesFakeTests組中。

HalfTunes項目包含了支持文件DHURLSessionMock.swift。這個文件中定義了一個簡單的協議——DHURLSession,其提供的方法(代理)用于使用一個URL或URLRequest來創建一個數據任務。它還定義了符合該協議的URLSessionMock對象,該對象中提供的初始化器可以讓你使用你選擇的數據、響應和誤差等來創造一個模擬URLSession對象。

現在,我們來構建偽造的數據和響應,并創建偽造的會話對象;這些都實現于方法setup()中,相應的代碼位于創建SUT對象的語句之后:

  1. let testBundle = Bundle(for: type(of: self)) 
  2. let path = testBundle.path(forResource: "abbaData", ofType: "json"
  3. let data = try? Data(contentsOf: URL(fileURLWithPath: path!), options: .alwaysMapped) 
  4.   
  5. let url = URL(string: "https://itunes.apple.com/search?media=music&entity=song&term=abba"
  6. let urlResponse = HTTPURLResponse(url: url!, statusCode: 200, httpVersion: nil, headerFields: nil) 
  7.   
  8. let sessionMock = URLSessionMock(data: data, response: urlResponse, error: nil) 
  9. At the end of setup(), inject the fake session into the app as a property of the SUT: 
  10.  
  11. controllerUnderTest.defaultSession = sessionMock 

 

【注意】您將直接在您的測試中使用偽造的會話,但是這將向你展示如何注入這種偽造的會話;這樣一來,你進一步的測試可以調用使用視圖控制器defaultSession屬性的SUT方法。

現在,您可以編寫測試來檢查是否調用updateSearchResults(_:)方法能夠解析偽造的數據。為此,請把TestExample()方法替換為以下內容︰

  1. //使用DHURLSession協議和代理偽造URLSession 
  2. func test_UpdateSearchResults_ParsesData() { 
  3.   // given 
  4.   let promise = expectation(description: "Status code: 200"
  5.   
  6.   // when 
  7.   XCTAssertEqual(controllerUnderTest?.searchResults.count, 0, "searchResults should be empty before the data task runs"
  8.   let url = URL(string: "https://itunes.apple.com/search?media=music&entity=song&term=abba"
  9.   let dataTask = controllerUnderTest?.defaultSession.dataTask(with: url!) { 
  10.     data, response, error in 
  11.     // if HTTP request is successful, call updateSearchResults(_:) which parses the response data into Tracks 
  12.     if let error = error { 
  13.       print(error.localizedDescription) 
  14.     } else if let httpResponse = response as? HTTPURLResponse { 
  15.       if httpResponse.statusCode == 200 { 
  16.         promise.fulfill() 
  17.         self.controllerUnderTest?.updateSearchResults(data) 
  18.       } 
  19.     } 
  20.   } 
  21.   dataTask?.resume() 
  22.   waitForExpectations(timeout: 5, handler: nil) 
  23.   
  24.   // then 
  25.   XCTAssertEqual(controllerUnderTest?.searchResults.count, 3, "Didn't parse 3 items from fake response"

 

注意,你仍然要以異步方式來編寫這個測試,因為代理(stub)假裝自己是一個異步的方法。

上面代碼中,when斷言的作用是:在數據任務運行之前searchResults的值應當是空的——這應該是真實情況,因為您在setup()方法中創建了一個全新的SUT。

偽造的數據包含了提供給三個跟蹤(Track)對象使用的JSON數據;所以,then斷言的作用是:視圖控制器的searchResults數組應當包含三項。

再次運行該測試。這次應該成功,而且速度很快,因為不存在任何真實的網絡連接!

偽造對模擬對象的更新

以前的測試使用代理從假對象提供輸入。接下來,你可以使用一個模擬對象來測試你的代碼可以正確更新UserDefaults。

重新打開BullsEye項目。注意到,該應用程序提供了兩種游戲風格:用戶可以選擇移動滑塊來匹配目標值或從滑塊位置猜測目標值。借助于界面右下角的分段控制開關可以切換游戲風格并更新用戶默認的游戲風格。

你要編寫的下一個測試將檢查應用程序能夠正確地更新用戶默認的游戲風格數據。

在測試導航器中,點擊命令“New Unit Test Target…”,并命名為BullsEyeMockTests。然后,在導入語句下面添加以下內容:

  1. @testable import BullsEye 
  2.   
  3. class MockUserDefaults: UserDefaults { 
  4.   var gameStyleChanged = 0 
  5.   override func set(_ value: Int, forKey defaultName: String) { 
  6.     if defaultName == "gameStyle" { 
  7.       gameStyleChanged += 1 
  8.     } 
  9.   } 

 

注意到,上面的MockUserDefaults類重載了set(_:forKey:)方法以便把gameStyleChanged標志的值加1。通常你會看到類似的測試中是設置一個布爾變量,但是在此我們使用一個整數值加1,這可以進一步增加你的靈活控制——例如你的測試可以檢查該方法僅被正確地調用一次。

在BullsEyeMockTests類中聲明SUT對象和模擬對象:

  1. var controllerUnderTest: ViewController! 
  2. var mockUserDefaults: MockUserDefaults! 

 

在方法setup()中,創建SUT對象和模擬對象,然后把此模擬對象注入為該SUT的一個屬性:

  1. controllerUnderTest = UIStoryboard(name"Main", bundle: nil).instantiateInitialViewController() as! ViewController! 
  2. mockUserDefaults = MockUserDefaults(suiteName: "testing")! 
  3. controllerUnderTest.defaults = mockUserDefaults 
  4. Release the SUT and the mock object in tearDown(): 
  5. controllerUnderTest = nil 
  6. mockUserDefaults = nil 
  7. Replace testExample() with this: 
  8. // Mock to test interaction with UserDefaults 
  9. func testGameStyleCanBeChanged() { 
  10.   // given 
  11.   let segmentedControl = UISegmentedControl() 
  12.   
  13.   // when 
  14.   XCTAssertEqual(mockUserDefaults.gameStyleChanged, 0, "gameStyleChanged should be 0 before sendActions"
  15.   segmentedControl.addTarget(controllerUnderTest,  
  16.       action: #selector(ViewController.chooseGameStyle(_:)), for: .valueChanged) 
  17.   segmentedControl.sendActions(for: .valueChanged) 
  18.   
  19.   // then 
  20.   XCTAssertEqual(mockUserDefaults.gameStyleChanged, 1, "gameStyle user default wasn't changed"

 

上述代碼中的when斷言的作用是:gameStyleChanged標志的值為0——在測試方法觸發分段控制開關之前。因此,如果then斷言也為真,那么將意味著方法set(_:forKey:)僅被正確地調用一次。

現在再次運行測試;應當可以成功。 

在Xcode中進行UI測試

Xcode 7中引入了對UI測試的支持,使您可以通過記錄與UI的交互來創建UI測試。UI測試的工作方式是:通過查詢來查找一個應用程序的UI對象,進而合成事件,然后將這些事件發送給這些對象。其提供的API使您可以檢查一個用戶界面對象的屬性和狀態,以便把它們與預期的狀態進行比較。

現在,讓我們在BullsEye項目的測試導航器中添加一個新的UI測試目標。確保要被測試的目標是BullsEye,然后接受默認名稱BullsEyeUITests。

然后,在BullsEyeUITests類的頂部添加如下屬性︰

  1. var app: XCUIApplication! 

在方法setup()中,用以下代碼替換XCUIApplication().launch()語句︰

  1. app = XCUIApplication() 
  2.  
  3. app.launch() 

 

把testExample()的名字更改為testGameStyleSwitch()。

然后,在testGameStyleSwitch()中按下回車鍵創建一個新的空行,并點擊編輯器窗口底部的紅色的Record按鈕,如圖所示。

當應用程序出現在模擬器中時,點擊控制游戲風格開關的滑動塊及頂部標簽。然后,單擊Xcode中的Record按鈕即可停止錄制。

現在,你在方法testGameStyleSwitch()中擁有以下三行代碼︰

  1. let app = XCUIApplication() 
  2.  
  3. app.buttons["Slide"].tap() 
  4.  
  5. app.staticTexts["Get as close as you can to: "].tap() 

 

如果還有其他的語句,則刪除它們。

第一行代碼的作用是復制你在setup()方法中創建的屬性;因為你還不需要點擊任何東西,所以也把這第一行刪除,還要刪除第2行與第3行末尾的“.tap()”。打開["Slide"]鄰近的小菜單并選擇segmentedControls.buttons["Slide"]。

于是,你有了如下的代碼:

  1. app.segmentedControls.buttons["Slide"
  2.  
  3. app.staticTexts["Get as close as you can to: "

 

進一步修改上述代碼,以便創建測試的given部分:

  1. // given 
  2.  
  3. let slideButton = app.segmentedControls.buttons["Slide"
  4.  
  5. let typeButton = app.segmentedControls.buttons["Type"
  6.  
  7. let slideLabel = app.staticTexts["Get as close as you can to: "
  8.  
  9. let typeLabel = app.staticTexts["Guess where the slider is: "

 

現在,你有了兩個按鈕和兩個可能的頂部標簽的名稱,再添加以下內容︰

  1. // then 
  2.  
  3. if slideButton.isSelected { 
  4.  
  5. XCTAssertTrue(slideLabel.exists) 
  6.  
  7. XCTAssertFalse(typeLabel.exists) 
  8.  
  9. typeButton.tap() 
  10.  
  11. XCTAssertTrue(typeLabel.exists) 
  12.  
  13. XCTAssertFalse(slideLabel.exists) 
  14.  
  15. else if typeButton.isSelected { 
  16.  
  17. XCTAssertTrue(typeLabel.exists) 
  18.  
  19. XCTAssertFalse(slideLabel.exists) 
  20.  
  21. slideButton.tap() 
  22.  
  23. XCTAssertTrue(slideLabel.exists) 
  24.  
  25. XCTAssertFalse(typeLabel.exists) 
  26.  

 

這段代碼將會檢測當選中或者點擊每個按鈕時是否存在正確的標簽。現在,運行測試——結果是所有斷言應該都成功。

性能測試

根據蘋果公司官方文檔

(https://developer.apple.com/library/prerelease/content/documentation/DeveloperTools/Conceptual/testing_with_xcode/chapters/04-writing_tests.html#//apple_ref/doc/uid/TP40014132-CH4-SW8)描述:一個性能測試需要使用你想要評估的一個代碼塊,并運行此代碼塊10次,期間收集平均執行時間和運行的標準偏差值。這些個別測量的平均值成為測試運行的一個值,然后把該值與一個基準值進行比較來評估成功或失敗。

寫一個性能測試還是非常簡單的︰你只需要把你想要測試的代碼放到measure()方法的閉包中即可。

為了實際體驗一下,請重新打開HalfTunes項目,然后在HalfTunesFakeTests類中使用下面的測試,從而替換掉系統默認生成的testPerformanceExample()方法︰

  1. // Performance  
  2. func test_StartDownload_Performance() { 
  3.   let track = Track(name"Waterloo", artist: "ABBA",  
  4.       previewUrl: "http://a821.phobos.apple.com/us/r30/Music/d7/ba/ce/mzm.vsyjlsff.aac.p.m4a"
  5.   measure { 
  6.     self.controllerUnderTest?.startDownload(track) 
  7.   } 

 

現在,請運行上面的測試,然后單擊measure()閉包末尾的圖標來觀看統計信息。

單擊“Set Baseline”(設置基準值)按鈕,然后再次運行性能測試并查看結果——結果有可能比基準值更好或更糟。你可以點擊Edit(編輯)按鈕幫助您將基準值重置為這個新的結果。

基準值在每個設備配置時存儲起來,所以你可以讓同一測試執行在若干臺不同的設備上,并使每臺設備保持一個不同的基準值——這要取決于處理器速度、內存等的具體配置情況。

任何時候只要你更改一個應用程序,都有可能影響正在測試的方法的性能;你可以再次運行性能測試來觀察當前值與基準值比較的結果。

代碼覆蓋

代碼覆蓋工具能夠告訴你應用程序中的哪些代碼實際上被您的測試運行過;這樣一來,你就可以知道應用程序代碼的哪些部分還沒有被測試。

【注意】在啟用代碼覆蓋功能時你是否應該運行性能測試呢?蘋果公司的文檔(https://developer.apple.com/library/prerelease/content/documentation/DeveloperTools/Conceptual/testing_with_xcode/chapters/07-code_coverage.html#//apple_ref/doc/uid/TP40014132-CH15-SW1)中是這樣描述的︰代碼覆蓋數據集合會導致性能的下降……以線性方式影響代碼的執行;因此,當啟用代碼覆蓋功能時程序的性能將會因不同的測試運行而有所差異。但是,當你對你的測試中的例程要求極其嚴格時你應該認真考慮是否要啟用代碼覆蓋支持。

為了啟用代碼覆蓋功能,你可以編輯一下你預先計劃的測試(Test)操作并勾選“Code Coverage”復選框︰

運行您的所有測試(按下組合鍵Command+U),然后打開報告導航器(按下組合鍵Command+8)。按執行時間先后選擇(By Time,見下圖)列表中最上面的一項,然后再選擇“Coverage”(覆蓋)選項卡。

你可以單擊如下圖展開的三角形圖標來觀察SearchViewController.swift文件中的函數列表︰

你可以把鼠標懸停在updateSearchResults(_:)方法附近的藍色的Coverage(覆蓋率)條上觀察到對應的覆蓋率為71.88%。

單擊該函數對應的箭頭按鈕來打開源文件,并定位到該函數。當你的鼠標移到右邊欄中的覆蓋率注釋上時,代碼段將突出顯示為綠色或紅色︰

覆蓋率注釋上的信息顯示出一個測試中命中每個代碼段的次數。注意,沒有被調用到的代碼段部分突出顯示為紅色。正如你所期望的,for循環運行3次,但沒有一次是沿著錯誤路徑執行的。為了提高此函數的代碼覆蓋率,你可以復制abbaData.json,然后修改它,使其會導致不同的錯誤——例如,將“results”更改為“result”來測試執行到打印語句print("Results key not found in dictionary")的情況。

100%覆蓋?

爭取實現100%的代碼覆蓋率你可知道應該付出怎樣的努力嗎?如果你使用谷歌搜索引擎搜索“100% unit test coverage”的話,你會搜索到有贊同的也有反對的等多種觀點,以及圍繞100%覆蓋率的大量爭論。其中,持反對看法的認為最后的10-15%并不重要——不值得為之付出努力;而持贊同看法的認為最后的10-15%極其重要——因為它很難測試。再使用谷歌搜索引擎搜索“hard to unit test bad design”可以找到頗有說服力的論據——無法驗證的代碼是一種更深層次的設計問題(https://www.toptal.com/qa/how-to-write-testable-code-and-why-it-matters)。進一步的思考可能導致的結論是測試驅動開發(http://qualitycoding.org/tdd-sample-archives/)是軟件開發過程中必須要走的路。

總結

本文中已經向你提供了為你的iOS工程編寫測試的多種工具。我希望你能夠通過本教程的學習樹立起足夠的信心來測試一切!

你可以從地址https://koenig-media.raywenderlich.com/uploads/2016/12/Finished-3.zip處下載本文中的完整的示例工程源碼。

最后,下面提供的一些資源可以供你作進一步學習測試使用:

 

  • 既然通過本文學習你已經學會了為你的項目編寫測試,那么你下一步要了解的應當是自動化測試相關的主題。為此,你可以首先學習蘋果官方的基于Xcode Server和xcodebuild的自動測試過程(Automating the Test Process,https://developer.apple.com/library/content/documentation/DeveloperTools/Conceptual/testing_with_xcode/chapters/08-automation.html#//apple_ref/doc/uid/TP40014132-CH7-SW1),以及發表在Wikipedia上的相關連載文章(https://en.wikipedia.org/wiki/Continuous_delivery),來源于ThoughtWorks網站(https://www.thoughtworks.com/continuous-delivery)上的一位資深專家的文章。
  • 使用Swift Playgrounds進行測試驅動開發(http://initwithstyle.net/2015/11/tdd-in-swift-playgrounds/)。你可以在Playgrounds環境下使用XCTestObservationCenter來運行XCTestCase單元測試。你可以在Playgrounds中開發你的工程代碼并進行測試,然后把二者都轉換成你的應用程序。
  • 來自CMD+U協會(http://www.cmduconf.com/)的教程告訴你如何使用PivotalCoreKit(https://github.com/pivotal/PivotalCoreKit)來測試watchOS應用程序。
  • 如果你已經編寫了一個應用程序,而只是沒有為它編寫測試,你可以參閱Michael Feathers的圖書《Working Effectively with Legacy Code》(https://www.amazon.com/Working-Effectively-Legacy-Michael-Feathers/dp/0131177052/ref=sr_1_1?s=books&ie=UTF8&qid=1481511568&sr=1-1),因為不包含測試的代碼往往都是遺留下來的代碼!
  • Jon Reid的高質量編碼示例編程文章(http://qualitycoding.org/tdd-sample-archives/)也是你學習測試驅動開發的極好去處。

【51CTO譯稿,合作站點轉載請注明原文譯者和出處為51CTO.com】

 

責任編輯:龐桂玉 來源: 51CTO.com
相關推薦

2020-05-07 17:30:49

開發iOS技術

2017-01-14 23:42:49

單元測試框架軟件測試

2017-02-23 15:59:53

測試MockSetup

2020-08-18 08:10:02

單元測試Java

2012-05-21 09:41:54

XcodeiOS單元測試

2017-01-16 12:12:29

單元測試JUnit

2017-01-14 23:26:17

單元測試JUnit測試

2011-07-27 17:02:12

Xcode iPhone 單元測試

2011-06-20 17:25:02

單元測試

2011-05-16 16:52:09

單元測試徹底測試

2017-03-23 16:02:10

Mock技術單元測試

2021-05-05 11:38:40

TestNGPowerMock單元測試

2010-10-09 14:43:46

J2MEJUnit

2011-07-04 18:16:42

單元測試

2023-07-26 08:58:45

Golang單元測試

2021-09-03 08:57:59

Swift強制解析

2023-08-02 13:59:00

GoogleTestCTest單元測試

2022-04-27 08:17:07

OCMock單元測試集成

2015-05-08 10:29:59

OCMockiOS測試

2009-09-01 10:20:06

protected方法單元測試
點贊
收藏

51CTO技術棧公眾號

亚洲最大的成人av| 美女国产一区| 亚洲成av人影院在线观看| 久久黄色片视频| 嫩草研究院在线| 麻豆久久久久久| 欧美精品成人在线| 摸摸摸bbb毛毛毛片| 欧美.com| 色婷婷香蕉在线一区二区| 99热这里只有精品7| 无套内谢的新婚少妇国语播放| 久久人人超碰| 欧美韩国理论所午夜片917电影| 三上悠亚影音先锋| 亚洲精品影片| 欧美日韩免费在线视频| 免费网站永久免费观看| 永久免费在线观看视频| 国产91综合网| 国产日韩精品在线| 六月丁香激情综合| 在线日本成人| 欧美成人精品在线视频| аⅴ天堂中文在线网| 丁香综合av| 91精品国产91久久综合桃花| 男女无套免费视频网站动漫| 久久久久久久久久久久久国产| 一区二区日韩视频| 亚洲一区网站| 欧美精品videos| 国产天堂av在线| heyzo久久| 日韩成人黄色av| 佐佐木明希电影| 91麻豆精品国产综合久久久| 欧美性极品xxxx做受| 麻豆传媒网站在线观看| 午夜免费福利在线观看| 国产日韩欧美制服另类| 久久国产精品一区二区三区四区| 国产高清免费观看| 国产美女视频一区| 91人人爽人人爽人人精88v| 中文字幕精品无码亚| 日韩专区欧美专区| 国产成人中文字幕| 色综合天天做天天爱| 久久久久久久一区二区| 波兰性xxxxx极品hd| 久久99国产成人小视频| 精品亚洲国产成av人片传媒| 制服丝袜av在线| 无人区乱码一区二区三区| 欧美精品久久99久久在免费线 | 亚洲图片欧美色图| 狠狠干视频网站| 黄色大片在线播放| 亚洲同性同志一二三专区| 伊人久久av导航| 日本韩国在线视频爽| 国产精品电影院| 在线精品亚洲一区二区| 国产在线看片| 一区二区三区四区五区视频在线观看 | 91av资源网| 日本在线高清| 欧美性猛交xxxx| 超碰av在线免费观看| 欧美日韩电影免费看| 欧美丝袜第一区| 国产综合免费视频| 欧美在线va视频| 欧美女孩性生活视频| 久久久久久综合网| 99热这里只有精品首页| 日韩黄色在线免费观看| 99久久人妻无码精品系列| 不卡一区综合视频| 久久视频国产精品免费视频在线| 永久免费看黄网站| 影音先锋久久| 国产精品精品视频| 99在线观看精品视频| 成人免费视频国产在线观看| 欧美13一14另类| 婷婷成人激情| 亚洲大尺度视频在线观看| 99视频在线免费播放| 一区二区三区电影大全| 欧洲性视频在线播放| 黑人狂躁日本娇小| 免费视频观看成人| 日韩一二三区不卡| 亚洲欧美在线不卡| 国产精品久久久久久麻豆一区软件| 北条麻妃在线一区二区| 国产精彩视频在线| 蜜臀av性久久久久蜜臀aⅴ| 91久久国产婷婷一区二区| 色婷婷av一区二区三| 国产精品伦理在线| 日韩伦理在线免费观看| 日韩美女在线| 亚洲精品wwww| 艳妇荡乳欲伦69影片| 欧美专区在线| 999久久久| 9色在线观看| 性感美女极品91精品| 久久这里只精品| 欧美日韩一区二区三区在线电影 | 欧美日韩蜜桃| 国产精品久久久久秋霞鲁丝| 少妇荡乳情欲办公室456视频| 国产精品入口麻豆原神| 亚洲不卡中文字幕无码| 国产电影一区二区| 中文国产成人精品| 久久久久久久久久久久久久av| 国产一区二区在线免费观看| 日韩高清dvd| 天堂中文在线播放| 精品日韩在线观看| 日本在线一级片| 日韩不卡一区二区| 欧美日韩一区二区三区在线视频| 免费在线观看的电影网站| 欧美精品亚洲二区| 欧美激情视频二区| 久久精品五月| 精品国产综合久久| 波多野结衣久久| 欧美一区二区黄色| 国产三级在线观看完整版| 国产精品毛片在线| 好看的日韩精品| 爱看av在线入口| 精品国产亚洲一区二区三区在线观看 | 视频一区中文字幕精品| 久久久999精品视频| 亚洲综合精品国产一区二区三区| 国产喷白浆一区二区三区| 天天摸天天碰天天添| 日本国产精品| 欧美亚州一区二区三区| 秋霞av在线| 一本到不卡精品视频在线观看| 成年人的黄色片| 性色av一区二区怡红| 国产专区一区二区三区| 亚洲深夜视频| 亚洲男人天天操| 黄色av一级片| 国产欧美一区二区在线| 欧美一级视频一区二区| 黄色一级片国产| videos性欧美另类高清| 日韩国产精品一区| 视频一区二区三区四区五区| 久久美女艺术照精彩视频福利播放| 青青草成人免费在线视频| 黑人久久a级毛片免费观看| 久久久久久香蕉网| 午夜性色福利影院| 91极品美女在线| 亚洲天堂精品一区| 国产伦精一区二区三区| 国内自拍中文字幕| 风间由美性色一区二区三区四区| 992tv在线成人免费观看| 亚洲欧美丝袜中文综合| 一本大道久久a久久精品综合| 公侵犯人妻一区二区三区| 天堂av在线一区| 一区二区三区欧美成人| 1204国产成人精品视频| 97超级碰碰碰| 国产1区2区3区在线| 欧美日韩成人综合| 久久艹精品视频| 国产亚洲视频系列| 亚洲午夜精品一区| 最新日韩欧美| 午夜精品区一区二区三| 成人av在线播放| 97精品国产97久久久久久春色| 日本在线视频1区| 欧美日韩国产综合视频在线观看| 麻豆成人在线视频| 国产丝袜美腿一区二区三区| 在线观看日本www| 国产精品视频| 99久久久无码国产精品性色戒| caoporn成人| 国产伦精品免费视频| 97在线视频免费观看完整版| 色偷偷偷综合中文字幕;dd| 精品人妻av一区二区三区| 色综合久久88色综合天天免费| 久久国产高清视频| 91在线观看下载| 色男人天堂av| 日日摸夜夜添夜夜添精品视频 | 亚洲av无码专区在线播放中文| 久久一综合视频| 精品人妻人人做人人爽| 欧美肉体xxxx裸体137大胆| 产国精品偷在线| 日本精品久久| 日本人成精品视频在线| 久草在线资源站资源站| 神马国产精品影院av| 日韩大片b站免费观看直播| 91麻豆精品国产91久久久久久久久| 日本中文在线播放| 亚洲永久精品大片| 久久人妻无码aⅴ毛片a片app| www日韩大片| 永久免费未满蜜桃| 国产馆精品极品| 国产精品拍拍拍| 国产精品日韩| 亚洲 欧美 日韩 国产综合 在线| 亚洲久久久久| 一区二区三区视频| 日本精品三区| 日韩精品国内| 亚洲免费专区| 好吊妞www.84com只有这里才有精品 | 成人免费视频网站| 国产一区二区三区免费在线| 国产精品视频大全| 精品免费av一区二区三区 | 国外成人福利视频| 日韩美女av在线免费观看| sm捆绑调教国产免费网站在线观看| 久久av资源网站| 欧美日韩欧美| 日韩在线欧美在线| 亚洲xxxxxx| 日韩一级黄色av| 3p视频在线观看| 日韩在线免费视频观看| 丝袜美腿美女被狂躁在线观看| 这里只有精品视频在线| 二区三区在线| 中文字幕少妇一区二区三区| eeuss影院在线观看| 在线一区二区日韩| 免费av在线播放| 免费av一区二区| 欧美卡一卡二| 国内精品久久久| 欧洲一区精品| 国产成人自拍视频在线观看| 欧洲av一区二区| 国产精品自产拍在线观看中文| 日韩av电影资源网| 国产日韩精品视频| 视频精品一区二区三区| 国产综合第一页| 欧美精美视频| 在线不卡视频一区二区| 亚洲乱码在线| 玩弄中年熟妇正在播放| 久久久999| 亚洲综合av在线播放| 国产高清精品网站| 麻豆精品国产传媒av| 久久先锋影音av鲁色资源| 免费黄在线观看| 亚洲免费av观看| 国产精品一区二区6| 一本久久综合亚洲鲁鲁五月天| 中文字幕第三页| 欧美成人精精品一区二区频| 污污网站在线免费观看| 亚洲最新中文字幕| 国产高清一区二区三区视频| 97色在线观看免费视频| 日韩美女在线看免费观看| 成人黄色片在线| 久久精品66| 亚洲欧美日产图| 亚洲高清激情| 91欧美视频在线| 成人丝袜视频网| 嘿嘿视频在线观看| 亚洲国产精品久久久男人的天堂| 福利网址在线观看| 日韩一区二区视频| 欧美孕妇孕交| 欧美成人精品在线播放| 中文字幕一区久| 91性高湖久久久久久久久_久久99| 欧美黑人做爰爽爽爽| 综合国产精品久久久| 国产精品最新自拍| 色哟哟在线观看视频| 久久影院电视剧免费观看| 粉嫩av性色av蜜臀av网站| 日韩欧美在线视频观看| 精品国产18久久久久久| 正在播放亚洲1区| av手机在线观看| 亚洲综合在线中文字幕| 青青视频一区二区| 国产一区二区精品在线观看| 久久久婷婷一区二区三区不卡| 黄色美女网站在线观看| 久久久成人精品视频| 亚洲电影观看| 国产精品一级久久久| 91综合久久一区二区| 日韩avxxx| 成人av免费在线观看| 精品综合在线| 秋霞影院一区二区三区| 91黄色在线看| 国产真实乱子伦精品视频| 一道本在线观看| 狠狠色噜噜狠狠狠狠97| 成人毛片在线免费观看| 欧美裸体男粗大视频在线观看| 97精品国产综合久久久动漫日韩| 久久久久久欧美精品色一二三四| 国产精品红桃| 日韩精品在线播放视频| 最新日韩av在线| 中文字幕在线观看欧美| 亚洲香蕉成视频在线观看 | 91爱视频在线| 国产视频1区2区3区| 不卡视频观看| 国产午夜精品在线观看| 国产精品久久国产精品| 深爱五月激情五月| 国产精品99久久久久| 久久成人精品视频| 国产精彩视频在线观看| 海角社区69精品视频| 国产sm精品调教视频网站| 亚洲国产私拍精品国模在线观看| 亚洲最大色综合成人av| 中日韩av在线| 一区二区福利| 欧美主播福利视频| 91丨九色丨海角社区| 激情五月婷婷综合| 国产精品综合不卡av| 欧美成人777| 欧美高清日韩| 欧美最猛性xxxxx(亚洲精品)| 亚洲图片欧美日韩| 久久黄色级2电影| 久久国产精品高清| www在线免费观看视频| 精品高清一区二区三区| 青少年xxxxx性开放hg| 亚洲另类欧美日韩| 亚洲精品一级| 日韩美女免费视频| 久草手机在线观看| 视频一区二区不卡| 日本阿v视频在线观看| 国产精品综合网| 久久精品波多野结衣| 亚洲国产精品福利| 亚洲欧洲日本韩国| 午夜老司机精品| 国产在线麻豆精品观看| 免费在线视频一区二区| 日韩电影中文字幕在线| 国产精品扒开腿做爽爽爽视频软件| 欧美一级爱爱| 狠狠狠色丁香婷婷综合激情| 久久久www成人免费毛片| 亚洲男人天堂网站| 亚洲久草在线| 女人天堂av手机在线| 中文字幕国产精品一区二区| 精品久久久久久亚洲综合网站| 高清欧美一区二区三区| 欧美日韩激情在线一区二区三区| 手机免费看av网站| 午夜不卡av免费| 在线看免费av| 国产一区二区在线观看免费播放| 久久精品一区| 青青草激情视频| 亚洲欧美日本另类| 精品国产亚洲日本| 97xxxxx| 中文字幕一区av| 日本天堂影院在线视频| 91在线直播亚洲| 老鸭窝亚洲一区二区三区| 国产成人av免费在线观看|