Unit test iOS

Unit test là một cách rất tốt để viết code dễ hơn, tốt hơn. Việc viết unit test sẽ giúp bạn tìm ra phần lớn các bug một cách dễ dàng và xử lý nó. Nhưng điều quan trọng hơn là làm thế nào để viết code để thật dễ dàng maintain?

1. Bắt đầu

  • Download start project tại link sau: Start Project.
  • Đây là basic project về contact có thể được kết nối với phía server. Mục đích ở đây là chúng ta cần phải viết code như một bài test để cho code được hoạt động đúng như mong đợi.
  • Bạn hãy tiến hành build và chạy để xem cách hoạt động của app như thế nào nhé. App này sử dụng core data để lưu contact.

iOS-Simulator-Screen-Shot-31.03.2015-21.55.29-333x500.png

  • Bạn đừng lo lắng về việc có hay không kinh nghiệm làm việc với core data.

2. Lợi ích và những bất lợi khi viết unit test

  • Bất lợi:
    • Phải viết thêm mã với các dự án mang tính thử nghiệm cao, mã viết để test có thể nhiều hơn mã chức năng
    • Khi có nhiều mã sẽ tốn nhiều chi phí cho việc maintain code
    • Càng viết code thì khối lượng bug càng nhiều vì bản chất viết mã test cũng là viết mã cho chức năng test
    • Mất nhiều thời gian viết, nhiều thời gian test, và bản thân mã để test cũng cần kiểm tra -> mất thêm chi phí
  • Lợi thế:
    • Bạn có thể tự tin chứng minh code của bạn hoạt động
    • Thông tin phản hồi nhanh chóng, bạn có thể sử dụng unit test để nhanh chóng validate code của bạn – những thứ rườm rà để kiểm tra bằng tay
    • Module hoá: để bạn viết được unit test thì bạn cũng cần phải module hoá code chức năng của bạn.
    • Tập trung: viết unit test giúp bạn tập trung vào các chi tiết nhỏ
    • Chắc chắn code bạn sẽ không bị phá vỡ bởi các bản sửa lỗi tiếp theo
    • Giúp xcode dễ dàng refactor

3. Cấu trúc của app

  • Bạn hãy mở project và nhìn vào project navigator có một vài chú ý:

Screen-Shot-2015-03-28-at-16.59.36.png

  • Class Person là một NSManagedObject chứa thông tin cơ bản về người. PersonInfo cũng chứa một số thông tin tương tự
  • PersonList có 3 file: một viewController, provider và data provider protocol -> 3 file này nhằm mục đích tránh một view controller quá lớn. Một cách khác trước đây mình có tìm hiểu đó là mô hình VIPER (một cách tốt để giảm thiểu massive view controller)
  • Trong trường hợp này, protocol được định nghĩa là PeopleListDataProviderProtocol.swift, bạn hãy mở nó ra và xem nó thế nào.
  • Một class phù hợp với protocol này có property là managedObjectContext, tableView, cùng với 2 func addPerson, fetch
  • View controller có tên PeopleListViewController có property là dataProvider (PeopleListDataProviderProtocol) nó sẽ được cài đặt trong AppDelegate. Bạn có thể addPeople bằng cách sử dụng ABPeoplePickerNavigationController. Class này cho phép bạn, các nhà phát triển truy cập danh bạ người dùng mà không cần sự cho phép rõ ràng.
  • PeopleListDataProvider có trách nhiệm giao tiếp với core data
  • Đó là overview, những thứ cần để bắt đầu viết unit test

4. Viết mocks

Mock cho phép bạn kiểm tra method hoặc property của bạn được thực hiện khi một điều gì đó xảy ra trong ứng dụng của bạn. Và bạn sẽ viết một unit test để kiểm tra xem điều gì thực sự xảy ra.

4.1. Chuẩn bị

  • Đầu tiên bạn cần chuẩn bị project để có thể testing
  • Chọn project -> Build Settings -> Search Defines Module -> Change setting to YES.

Screen-Shot-2015-03-28-at-17.40.12-700x195.png

  • Tiếp theo bạn chọn thư mục BirthdaysTests -> File/New/File -> iOS/Source/Test Case Class template -> Next -> đặt tên là PeopleListViewControllerTests -> Click Next -> Cuối cùng chọn creat.
  • Bạn thêm instance vài PeopleListViewController bằng cách add thêm method setUp() như sau:
override func setUp() {
  super.setUp()
 
  viewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewControllerWithIdentifier("PeopleListViewController") as! PeopleListViewController
}
  • Bạn chọn Product/Test -> build project và chạy. Mặc dù bạn không có unit test nào nhưng đảm bảo được mọi thứ thiết lập đúng, sau một vài đơn vị thời gian bạn cần đảm bảo run success
  • Tới đây bạn đã sẵn sàng cho mock đầu tiên.

4.2. Viết mock đầu tiên

class MockDataProvider: NSObject, PeopleListDataProviderProtocol {
 
  var managedObjectContext: NSManagedObjectContext?
  weak var tableView: UITableView!
  func addPerson(personInfo: PersonInfo) { }
  func fetch() { }
  func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return 1 }
  func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    return UITableViewCell()
  }
}
  • Nhìn nó có vẻ như một class rất phức tạp, tuy nhiên nó chỉ là yêu cầu tối thiểu
  • Bạn chọn Product/Test và run lại một lần nữa, lại một lần nữa ứng dụng chạy ko lỗi
func testDataProviderHasTableViewPropertySetAfterLoading() {
  // given
  // 1
  let mockDataProvider = MockDataProvider()
 
  viewController.dataProvider = mockDataProvider
 
  // when
  // 2
  XCTAssertNil(mockDataProvider.tableView, "Before loading the table view should be nil")
 
  // 3
  let _ = viewController.view
 
  // then
  // 4
  XCTAssertTrue(mockDataProvider.tableView != nil, "The table view should be set")
  XCTAssert(mockDataProvider.tableView === viewController.tableView,
    "The table view should be set to the table view of the data source")
}
  • Tạo một instance của MockDataProvider và đặt nó vào thuộc tính của dataProvider

  • Khẳng định tableView nil trước khi thực hiện

  • Kích hoạt viewDidLoad

  • Khẳng định tableView lúc này không còn là nil

  • Tiếp theo bạn hãy chạy Product/Test một lần nữa (cmd + 5) và kết qủa như sau:

Screen-Shot-2015-03-29-at-10.54.20.png

  • Nếu kết quả như hình vẽ tức là mock của bạn passed

4.3. Test addPerson method

func testCallsAddPersonOfThePeopleDataSourceAfterAddingAPersion() {
  // given
  let mockDataSource = MockDataProvider()
 
  // 1
  viewController.dataProvider = mockDataSource
 
  // when
  // 2
  let record: ABRecord = ABPersonCreate().takeRetainedValue()
  ABRecordSetValue(record, kABPersonFirstNameProperty, "TestFirstname", nil)
  ABRecordSetValue(record, kABPersonLastNameProperty, "TestLastname", nil)
  ABRecordSetValue(record, kABPersonBirthdayProperty, NSDate(), nil)
 
  // 3
  viewController.peoplePickerNavigationController(ABPeoplePickerNavigationController(),
    didSelectPerson: record)
 
  // then
  // 4
  XCTAssert(mockDataSource.addPersonGotCalled, "addPerson should have been called")
}
  • Các bước được thực hiện ở đây cũng không khác gì method trước, đầu tiên là thiết lập để cung cấp dữ liệu khởi tạo, tiếp theo khẳng định nil trước khi thực hiện, kích hoạt và khẳng định khác nil sau khi thực hiện
  • Các bạn tự run và kiểm tra kết quả nhé

5. Mocking Apple Framework Classes

  • Bạn có thể sử dụng singleton như NSNotificationCenter.defaultCenter() nhưng làm thế nào có thể test chúng trong khi Apple không cho phép bạn kiểm tra trạng thái class này?.
  • Bạn có thể sử dụng addObserver nhưng điều này có thể gây ra cho unit test của bạn trở lên phức tạp và chậm chạp
func testSortingCanBeChanged() {
  // given
  // 1
  let mockUserDefaults = MockUserDefaults(suiteName: "testing")!
  viewController.userDefaults = mockUserDefaults
 
  // when
  // 2
  let segmentedControl = UISegmentedControl()
  segmentedControl.selectedSegmentIndex = 0
  segmentedControl.addTarget(viewController, action: "changeSorting:", forControlEvents: .ValueChanged)
  segmentedControl.sendActionsForControlEvents(.ValueChanged)
 
  // then
  // 3
  XCTAssertTrue(mockUserDefaults.sortWasChanged, "Sort value in user defaults should be altered")
}

6. Viết Stubs

  • Stubs giả là một phản ứng với lời gọi phương thức của một đố tượng. Bạn có thể sử dụng để test mã code của bạn khi có sử dụng request network

  • Đội xây dựng web được giao nhiệm vụ xây dựng trang web với các chức năng tương tự ứng dụng của bạn. Người dung tạo 1 tài khoản và đồng bộ dữ liệu giữa ứng dụng và trang web. Nhưng nhóm web đó chưa bắt đầu để hoàn tất. Khi đó bạn cần 1 stub để bắt đầu phụ trợ cho web đó.

  • Đó là một vài điều mình chia sẻ vể stubs

Note:

  • Các bạn có thể download project tại Finished project
  • Chính vì những điểm mạnh và điểm yếu của việc viết unit test nên tuỳ thuộc vào tình trạng dự án thế nào mà các bạn quyết định có hay không viết unit test nhé.