利用面向協議的程式設計進行單元測試

面向協議程式設計是一個有用的工具,可以輕鬆地為我們的程式碼編寫更好的單元測試。

假設我們想要測試一個依賴於 ViewModel 類的 UIViewController。

生產程式碼所需的步驟是:

  1. 定義一個公開 ViewModel 類的公共介面的協議,以及 UIViewController 所需的所有屬性和方法。
  2. 實現真正的 ViewModel 類,符合該協議。
  3. 使用依賴注入技術讓檢視控制器使用我們想要的實現,將其作為協議而不是具體例項傳遞。
protocol ViewModelType {
   var title : String {get}
   func confirm()
}

class ViewModel : ViewModelType {
   let title : String

   init(title: String) {
       self.title = title
   }
   func confirm() { ... }
}

class ViewController : UIViewController {
   // We declare the viewModel property as an object conforming to the protocol
   // so we can swap the implementations without any friction.
   var viewModel : ViewModelType! 
   @IBOutlet var titleLabel : UILabel!

   override func viewDidLoad() {
       super.viewDidLoad()
       titleLabel.text = viewModel.title
   }

   @IBAction func didTapOnButton(sender: UIButton) {
       viewModel.confirm()
   }
}

// With DI we setup the view controller and assign the view model.
// The view controller doesn't know the concrete class of the view model, 
// but just relies on the declared interface on the protocol.
let viewController = //... Instantiate view controller
viewController.viewModel = ViewModel(title: "MyTitle")

然後,在單元測試:

  1. 實現符合相同協議的模擬 ViewModel
  2. 使用依賴注入將其傳遞給測試中的 UIViewController,而不是真實例項。
  3. 測試!
class FakeViewModel : ViewModelType {
   let title : String = "FakeTitle"

   var didConfirm = false
   func confirm() {
       didConfirm = true
   }
}

class ViewControllerTest : XCTestCase {
    var sut : ViewController!
    var viewModel : FakeViewModel!

    override func setUp() {
        super.setUp()

        viewModel = FakeViewModel()
        sut = // ... initialization for view controller
        sut.viewModel = viewModel

        XCTAssertNotNil(self.sut.view) // Needed to trigger view loading
    } 

    func testTitleLabel() {
        XCTAssertEqual(self.sut.titleLabel.text, "FakeTitle")
    }

    func testTapOnButton() {
        sut.didTapOnButton(UIButton())
        XCTAssertTrue(self.viewModel.didConfirm)
    }
}