新聞中心
概覽
單元測試使用 XCTest 來編寫,可讓你確信更改和添加內(nèi)容不會導(dǎo)致 App 功能下降,從而加快你的開發(fā)速度。在現(xiàn)有項(xiàng)目中添加單元測試可能比較難,因?yàn)槿绻龀龅脑O(shè)計(jì)選擇沒有考慮可測試性,可能會使不同的類或子系統(tǒng)耦合在一起,導(dǎo)致無法將它們分開測試。軟件設(shè)計(jì)中的耦合表現(xiàn)為某個類或函數(shù)只有在與以特定方式工作的其他代碼連接時才能成功地使用。有時,這種耦合意味著你的測試會嘗試連接網(wǎng)絡(luò)或與文件系統(tǒng)交互,而這會造成測試速度減慢并讓結(jié)果變得不確定。移除耦合后便可以引入單元測試,但需要你在測試還沒有覆蓋的位置上進(jìn)行代碼更改,而這可能存在風(fēng)險。

創(chuàng)新互聯(lián)專注于企業(yè)成都全網(wǎng)營銷、網(wǎng)站重做改版、宜城網(wǎng)站定制設(shè)計(jì)、自適應(yīng)品牌網(wǎng)站建設(shè)、H5高端網(wǎng)站建設(shè)、成都做商城網(wǎng)站、集團(tuán)公司官網(wǎng)建設(shè)、外貿(mào)網(wǎng)站制作、高端網(wǎng)站制作、響應(yīng)式網(wǎng)頁設(shè)計(jì)等建站業(yè)務(wù),價格優(yōu)惠性價比高,為宜城等各大城市提供網(wǎng)站開發(fā)制作服務(wù)。
通過確定你想要測試的組件,并編寫能涵蓋你要斷言的行為的測試用例,改進(jìn)你項(xiàng)目中的測試覆蓋范圍。利用以風(fēng)險導(dǎo)向的優(yōu)先級確定方法,確保測試覆蓋以下兩類功能中的邏輯:收到過大量用戶錯誤報告的功能,或者性能下降有最大影響的功能。
如果你要測試的代碼與項(xiàng)目的另一部分或某個框架類耦合,請盡可能減少對這個代碼的更改,在不改變其行為的前提下隔離這個組件。在減少耦合的前提下,提升在測試環(huán)境中使用相應(yīng)類的能力,并僅稍作調(diào)整,以降低與各項(xiàng)更改相關(guān)的風(fēng)險。
以下幾個部分提供了一些更改建議,介紹了在待測代碼與其他組件之間的耦合妨礙測試時如何移除這些耦合。每種解決方案均展示了 XCTest 用例如何與更改后的代碼配合來斷言其行為。
將具體類型替換為協(xié)議
如果你的代碼依賴于特定的類,而這個類的行為會導(dǎo)致測試?yán)щy,請創(chuàng)建一個協(xié)議來列出相應(yīng)代碼使用的方法和屬性。這種問題依賴項(xiàng)的示例包括訪問外部狀態(tài)的依賴項(xiàng),如用戶文稿或數(shù)據(jù)庫等,也包括沒有確定性結(jié)果的依賴項(xiàng),如網(wǎng)絡(luò)連接或隨機(jī)值生成器等。
以下摘錄顯示了 Cocoa App 中的一個類,該類使用 NSWorkspace (英文) 來打開作為電子郵件或即時信息附件的文件。openAttachment(file:in:) 方法的結(jié)果取決于用戶有沒有安裝能處理所請求類型的文件的 App,以及這個 App 能不能成功打開文件。所有這些變量都有可能導(dǎo)致測試失敗,并由于需調(diào)查的“錯誤”可能是與你的代碼無關(guān)的瞬態(tài)問題,從而減慢你開發(fā)的速度。
import Cocoa enum AttachmentOpeningError : Error { case UnableToOpenAttachment } class AttachmentOpener { func openAttachment(file location: URL, with workspace: NSWorkspace) throws { if (!workspace.open(location)) { throw AttachmentOpeningError.UnableToOpenAttachment } } }
要測試具有這種耦合的代碼,可引入一個協(xié)議來描述你的代碼如何與問題依賴項(xiàng)交互。在你的代碼中使用該協(xié)議,使得你的類依賴于相關(guān)方法在協(xié)議中的存在性,而不是它們的具體實(shí)現(xiàn)。為協(xié)議編寫不執(zhí)行有狀態(tài)或非確定性任務(wù)的可選實(shí)現(xiàn),并使用該實(shí)現(xiàn)來編寫具有受控行為的測試。
以下摘錄中定義了一個包含 open(_:) 方法的協(xié)議,以及一個 NSWorkspace 擴(kuò)展來使得 NSWorkspace 遵從該協(xié)議。
import Cocoa enum AttachmentOpeningError : Error { case UnableToOpenAttachment } protocol URLOpener { func open(_ file: URL) -> Bool } extension NSWorkspace : URLOpener {} class AttachmentOpener { func openAttachment(file location: URL, with workspace: URLOpener) throws { if (!workspace.open(location)) { throw AttachmentOpeningError.UnableToOpenAttachment } } }
在測試中,為 URLOpener 協(xié)議另外編寫一個不依賴于用戶電腦上所裝 App 的實(shí)現(xiàn)代碼。
class StubWorkspace : URLOpener { func open(_ file: URL) -> Bool { return isSuccessful } var isSuccessful: Bool = true } class AttachmentOpenerTests: XCTestCase { var workspace: StubWorkspace! = nil var attachmentOpener: AttachmentOpener! = nil let location = URL(fileURLWithPath: "/tmp/a_file.txt") override func setUp() { workspace = StubWorkspace() attachmentOpener = AttachmentOpener() } override func tearDown() { workspace = nil attachmentOpener = nil } func testWorkspaceCanOpenAttachment() { workspace.isSuccessful = true XCTAssertNoThrow(try attachmentOpener.openAttachment(file: location, with: workspace)) } func testThrowIfWorkspaceCannotOpenAttachment() { workspace.isSuccessful = false XCTAssertThrowsError(try attachmentOpener.openAttachment(file: location, with: workspace)) } }
將指定類型替換為元類型值
如果 App 中的某個類會創(chuàng)建并使用另一個類的實(shí)例,并且創(chuàng)建的對象會帶來測試難點(diǎn),那么可能很難在創(chuàng)建的位置上測試該類。對所創(chuàng)建對象的類型進(jìn)行參數(shù)化處理,并使用必要的構(gòu)造器來創(chuàng)建實(shí)例。這種棘手測試情形的示例包括用一個控制器響應(yīng)用戶操作來在文件系統(tǒng)上創(chuàng)建新文稿,或者用一個方法來解釋從 Web 服務(wù)收到的 JSON 并創(chuàng)建代表所接收數(shù)據(jù)的新 Core Data 托管對象。
在以上各種情形中,由于相關(guān)對象由你想要測試的代碼創(chuàng)建,你無法將另一個對象作為參數(shù)傳遞給對應(yīng)方法。該對象只有在你的代碼創(chuàng)建它后才會存在,這時它所屬的類型具有不可測試的行為。
以下摘錄中顯示了一個 UIDocumentBrowserViewControllerDelegate (英文),它會在用戶從瀏覽器中選取文稿時創(chuàng)建并打開一個文稿對象。它創(chuàng)建的文稿對象會從文件系統(tǒng)讀取數(shù)據(jù)并向文件系統(tǒng)寫入數(shù)據(jù),因此難以在單元測試中控制它的行為。
class DocumentBrowserDelegate : NSObject, UIDocumentBrowserViewControllerDelegate { func documentBrowser(_ controller: UIDocumentBrowserViewController, didPickDocumentsAt documentURLs: [URL]) { guard let sourceURL = documentURLs.first else { return } let storyBoard = UIStoryboard(name: "Main", bundle: nil) let documentViewController = storyBoard.instantiateViewController(withIdentifier: "DocumentViewController") as! DocumentViewController documentViewController.document = Document(fileURL: sourceURL) documentViewController.modalPresentationStyle = .fullScreen controller.present(documentViewController, animated: true, completion: nil) } }
要移除你要嘗試測試的代碼和它所創(chuàng)建的對象之間的耦合,請?jiān)诖郎y類上定義一個變量來表示它應(yīng)構(gòu)造的對象的“類型”。這類變量稱為“元類型值”。將默認(rèn)值設(shè)為該類所使用的類型。你需要確保用于構(gòu)造實(shí)例的構(gòu)造器標(biāo)記為 required。以下摘錄顯示了引入該變量的文稿瀏覽器視圖控制器委托。該委托會創(chuàng)建類型由該元類型值定義的文稿。
class DocumentBrowserDelegate : NSObject, UIDocumentBrowserViewControllerDelegate { var DocumentClass = Document.self func documentBrowser(_ controller: UIDocumentBrowserViewController, didPickDocumentsAt documentURLs: [URL]) { guard let sourceURL = documentURLs.first else { return } let storyBoard = UIStoryboard(name: "Main", bundle: nil) let documentViewController = storyBoard.instantiateViewController(withIdentifier: "DocumentViewController") as! DocumentViewController documentViewController.document = DocumentClass.init(fileURL: sourceURL) documentViewController.modalPresentationStyle = .fullScreen controller.present(documentViewController, animated: true, completion: nil) } }
在測試中為該元類型設(shè)置一個不同的值,這樣你的代碼就能構(gòu)造沒有同樣不可測試行為的對象。在測試中,為文稿類創(chuàng)建一個“試驗(yàn)虛擬”版本:這個類具有相同的接口,但不實(shí)現(xiàn)難以測試的行為。在這個用例中,虛擬文稿類不應(yīng)與文件系統(tǒng)交互。
class DummyDocument : Document { static var opensSuccessfully:Bool = true static var savesSuccessfully:Bool = true static var closesSuccessfully:Bool = true override func save(to url: URL, for saveOperation: UIDocument.SaveOperation, completionHandler: ((Bool) -> Void)? = nil) { // don't save anything, just call the completion handler guard let handler = completionHandler else { return } handler(StubDocument.savesSuccessfully) } override func close(completionHandler: ((Bool) -> Void)? = nil) { guard let handler = completionHandler else { return } handler(StubDocument.closesSuccessfully) } override func open(completionHandler: ((Bool) -> Void)? = nil) { guard let handler = completionHandler else { return } handler(StubDocument.opensSuccessfully) } }
在測試用例的 setUp() 方法中,將文稿類型替換為虛擬類型,使待測委托創(chuàng)建虛擬文稿類型的實(shí)例,這些實(shí)例在測試中具有確定的行為。
class DocumentBrowserDelegateTests: XCTestCase { var delegate: DocumentBrowserDelegate! = nil override func setUp() { delegate = DocumentBrowserDelegate() delegate.DocumentClass = StubDocument.self } override func tearDown() { } // test methods here }
將不可測試的方法歸為子類并加以覆蓋
如果一個類結(jié)合運(yùn)用自定邏輯與導(dǎo)致該類難以測試的交互或行為,請引入一個子類來覆蓋該類的一部分方法,從而讓其余部分變得更易測試。類設(shè)計(jì)中常常會同時包含特定于某個 App 的邏輯以及與環(huán)境或框架的交互,而后者帶來的行為在測試中難以控制。一個常見的示例是 UIViewController (英文) 子類,該子類在其操作方法中包含特定于 App 的代碼,同時也會載入視圖或提供其他視圖控制器。
我們需要對自定 App 邏輯進(jìn)行測試,確保此邏輯能按預(yù)期運(yùn)行并防止出現(xiàn)性能下降??刂苹蛱幚眍惻c環(huán)境之間的交互比較復(fù)雜,導(dǎo)致相關(guān)邏輯難以測試。
例如,以下 iOS 視圖控制器使用在描述文件對象中找到的用戶名來填充標(biāo)簽 (并且大體上,它也可以使用描述文件中的其他欄位來填充其他 UI 元素)。它使用 UserDefaults (英文) 查找文件的路徑,嘗試將該文件載入為字典,然后使用該字典中的值來填充 UI。
struct UserProfile { let name: String } class ProfileViewController: UIViewController { @IBOutlet var nameLabel: UILabel! override func viewDidLoad() { self.loadProfile() { maybeUser in if let user = maybeUser { self.nameLabel.text = user.name } else { self.nameLabel.text = "Unknown User" } } } func loadProfile(_ completion: (UserProfile?) -> ()) { let path = UserDefaults.standard.string(forKey: "ProfilePath") guard let thePath = path else { completion(nil) return } let profileURL = URL(fileURLWithPath: thePath) let profileDict = NSDictionary(contentsOf: profileURL) guard let profileDictionary = profileDict else { completion(nil) return } guard let userName = profileDictionary["Name"] else { completion(nil) return } let profile = UserProfile(name: userName as! String) completion(profile) } }
要解決這種復(fù)雜性,請將你的視圖控制器歸為子類,并通過用更簡單的方法覆蓋來“剔除”產(chǎn)生復(fù)雜并不可測試交互的方法。在你的測試中使用該子類來驗(yàn)證你沒有覆蓋的自定邏輯的行為。如果待測代碼會創(chuàng)建目標(biāo)類型的實(shí)例,你也可能需要引入元類型值。
以下摘錄中引入了子類 StubProfileViewController,該子類與 UserDefaults 以及父類中的文件系統(tǒng)之間沒有任何耦合。相反,它使用了由調(diào)用方配置的 UserProfile 對象。使用此子類的測試可以輕松而準(zhǔn)確地提供所需的對象來觸發(fā)要測試的邏輯。
class StubProfileViewController: ProfileViewController { var loadedProfile: UserProfile? = nil override func loadProfile(_ completion: (UserProfile?) -> ()) { completion(loadedProfile) } }
需要兩個測試才能完全覆蓋 viewDidLoad() 的行為。其中一個測試在描述文件可以載入時,檢查是否從描述文件中正確設(shè)置了名稱。另一個測試在描述文件不能載入時,檢查是否使用了名稱的占位符值。
class ProfileViewControllerTests: XCTestCase { var profileVC: StubProfileViewController! = nil override func setUp() { profileVC = StubProfileViewController() // configure the label, in lieu of loading a storyboard profileVC.nameLabel = UILabel(frame: CGRect.zero) } override func tearDown() { } func testSuccessfulProfileLoadingSetsNameLabel() { profileVC.loadedProfile = UserProfile(name: "User Name") profileVC.viewDidLoad() XCTAssertEqual(profileVC.nameLabel.text, "User Name") } func testFailedProfileLoadingUsesPlaceholderLabelValue() { profileVC.loadedProfile = nil profileVC.viewDidLoad() XCTAssertEqual(profileVC.nameLabel.text, "Unknown User") } }
注釋
這種模式有助于你測試包含多項(xiàng)職責(zé)的現(xiàn)有類,但在從頭開始設(shè)計(jì)可測試的代碼時這并不是一種好的方法。將處理不同問題的代碼劃分到不同的類中;例如,創(chuàng)建一個控制器類來包含你的 App 邏輯,同時創(chuàng)建一個單獨(dú)的視圖控制器來協(xié)調(diào) UIKit 和 App 相關(guān)行為。在涵蓋單元測試中所剔除邏輯的端到端工作流程中,添加 UI 測試來驗(yàn)證真實(shí)類的行為。
在重新設(shè)計(jì)現(xiàn)有代碼時,第一步是將不可測試的方法歸為子類并加以覆蓋,從而將 App 邏輯和與框架或外部數(shù)據(jù)的整合分隔開來。通過以這種方式分隔代碼,不僅能更加輕松地了解你的項(xiàng)目中哪些部分用于實(shí)現(xiàn) App 的功能,哪些部分與系統(tǒng)的其余部分集成,而且還能在你更改代碼以利用新 API 或采用其他技術(shù)時降低造成邏輯錯誤的幾率。
注入單一實(shí)例
如果你的代碼使用單一實(shí)例對象來獲取對全局狀態(tài)或行為的訪問,請將該單一實(shí)例轉(zhuǎn)換為可被替換的參數(shù),來支持測試所需的隔離。單一實(shí)例的使用可能會分散到整個代碼庫中,這會導(dǎo)致在由你要嘗試測試的組件使用時,難以知曉單一實(shí)例所處的狀態(tài)。以不同的順序運(yùn)行測試可能會產(chǎn)生不同的結(jié)果。
常用的單一實(shí)例 (包括 NSApplication 和默認(rèn)的 FileManager) 具有依賴于外部狀態(tài)的行為。使用這些單一實(shí)例的組件會直接給可靠測試帶來更多復(fù)雜因素。
在這個示例中,一個 Cocoa 視圖控制器代表某新聞 App 中文稿檢查器的一部分。當(dāng)該視圖控制器代表的對象發(fā)生改變時,它會發(fā)布一個通知到 App 中其他組件訂閱的默認(rèn)通知中心。
let InspectedArticleDidChangeNotificationName: String = "InspectedArticleDidChange" class ArticleInspectorViewController: NSViewController { var article: Article! = nil override var representedObject: Any? { didSet { article = representedObject as! Article? NotificationCenter.default.post(name: NSNotification.Name(rawValue: InspectedArticleDidChangeNotificationName), object: article, userInfo: ["Article": article!]) } } }
盡管測試可以在默認(rèn)通知中心中注冊來觀察此通知,但由于使用了單一實(shí)例通知中心,App 中的其他組件有可能會干擾測試結(jié)果。其他代碼可能會發(fā)布通知,移除觀察器,或運(yùn)行自己的代碼來響應(yīng)通知,所有這些都可能會干擾測試的結(jié)果。
將對單一實(shí)例對象的直接訪問替換為可從待測組件外部控制的參數(shù)或?qū)傩?。?App 中,繼續(xù)將單一實(shí)例用作組件的協(xié)作者。在測試中,提供更易控制的替代對象。
以下摘錄顯示了將此更改應(yīng)用到上述文章檢查器視圖控制器后的結(jié)果。該視圖控制器將通知發(fā)布到其 notificationCenter 屬性中定義的通知中心,該屬性初始化為默認(rèn)的中心。
let InspectedArticleDidChangeNotificationName: String = "InspectedArticleDidChange" class ArticleInspectorViewController: NSViewController { var article: Article! = nil var notificationCenter: NotificationCenter = NotificationCenter.default override var representedObject: Any? { didSet { article = representedObject as! Article? notificationCenter.post(name: NSNotification.Name(rawValue: InspectedArticleDidChangeNotificationName), object: self, userInfo: ["Article": article!]) } } }
在測試用例中,你可以替換為不同的通知中心,該通知中心不在測試套件或 App 的其余部分中使用,因而與其他測試和模塊的行為隔離開來。
class ArticleInspectorViewControllerTests: XCTestCase { var viewController: ArticleInspectorViewController! = nil var article: Article! = nil var notificationCenter: NotificationCenter! = nil var articleFromNotification: Article! = nil var observer: NSObjectProtocol? = nil override func setUp() { notificationCenter = NotificationCenter() viewController = ArticleInspectorViewController() viewController.notificationCenter = notificationCenter article = Article() observer = notificationCenter.addObserver(forName: NSNotification.Name(InspectedArticleDidChangeNotificationName), object: viewController, queue: nil) { note in let observedArticle = note.userInfo?["Article"] self.articleFromNotification = observedArticle as? Article } } override func tearDown() { notificationCenter.removeObserver(observer!) } func testNotificationSentOnChangingInspectedArticle() { viewController.representedObject = self.article XCTAssertEqual(self.articleFromNotification, self.article, "Notification should have been posted") } }
你可能需要將此更改與本文“將具體類型替換為協(xié)議”和“將不可測試的方法歸為子類并加以覆蓋”部分中所述的更改相結(jié)合,以創(chuàng)建你在測試中用于取代單一實(shí)例的替代對象。當(dāng)單一實(shí)例帶來的行為難以在測試中控制時,如 FileManager 或 NSApplication 等單一實(shí)例,你就需要這樣做。
本文名稱:創(chuàng)新互聯(lián)IOS教程:在現(xiàn)有項(xiàng)目中添加單元測試
本文路徑:http://m.5511xx.com/article/cosesio.html


咨詢
建站咨詢
