12/08/2018, 13:44

TDD in iOS development

1. Giới thiệu: TDD là gì? TDD, viết tắt của Test-Driven Development là một tiến trình phát triển phần mềm dựa trên nguyên tắc lặp lại của một vòng đời phát triển ngắn: các yêu cầu được chuyển đổi thành những test case cụ thể, sau đó các function càng đơn giản càng tốt sẽ được viết ra để pass ...

1. Giới thiệu:

TDD là gì?

TDD, viết tắt của Test-Driven Development là một tiến trình phát triển phần mềm dựa trên nguyên tắc lặp lại của một vòng đời phát triển ngắn: các yêu cầu được chuyển đổi thành những test case cụ thể, sau đó các function càng đơn giản càng tốt sẽ được viết ra để pass những test case này và quay lại refactor các function sao cho nó ngắn gọn, logic hơn, cũng như tổ chức code tốt hơn. Điều này sẽ giúp ta không tạo ra những chức năng dư thừa không đáp ứng được yêu cầu ban đầu.

Trong TDD, mỗi chức năng bắt đầu bằng việc viết test. Để viết một test case, ta phải hiểu rõ về yêu cầu của chức năng, các ngoại lệ thông qua các use cases và user stories. Điều này ngược lại so với unit test là unit test được viết sau khi code đã hoàn thành. Vì vậy TDD giúp ta tập trung vào requirements trước khi bắt tay vào code.

Tầm quan trọng của TDD?

TDD giúp tạo ra những readable-code với design tốt, dễ dàng bảo trì, phần mềm thực hiện đúng theo những chức năng yêu cầu.

2. Demo:

Để hiểu rõ hơn việc áp dụng TDD vào một ứng dụng iOS, hãy cùng tạo ra một demo nhỏ về AddressBook lấy dữ liệu từ server. Sau khi app được chạy lên, contact view hiện ra, app sẽ lấy dữ liệu từ server về, ta sẽ thực hiện testing việc này. Trong phạm vi demo, ta không tập trung vào việc lấy dữ liệu mà chỉ cung cấp những function giả.

a. Tạo project:

Trước hết, tạo ra một single project đặt tên nó là AddressBookWithTDD, tích vào "Include Unit Tests".

Screen Shot 2016-08-20 at 21.53.18.png

Project được tạo ra gồm hai phần, AddressBookWithTDD chứa code chính của chương trình và AddressBookWithTDDTest chứa những test cases.

Screen Shot 2016-08-20 at 21.55.36.png

b. Thực hiện testing:

Các test cases sẽ được viết trước, vì vậy trước tiên, tạo ra file test ContactsViewControllerTest cho class ContactViewController. Tạo đối tượng của ContactViewController, sau đó khởi tạo giá trị, release nó trước và sau mỗi test case:

Screen Shot 2016-08-20 at 23.55.27.png

Compiler sẽ báo lỗi Not found ở ContactsViewController, nên hãy tạo class ContactsViewController trong app target:

Screen Shot 2016-08-20 at 22.16.09.png

Bây giờ code không còn lỗi nữa nhưng nó vẫn chưa test chức năng nào. Hãy bắt tay vào viết test case đầu tiên test việc request tất cả các contact từ server một khi ContactViewController xuất hiện:

  • Trước khi controller xuất hiện, sẽ không có bất kỳ một request nào đang được thực thi
  • Sau khi xuất hiện, sẽ có một request thuộc kiểu DataProviderAllContacts đang được thực thi

Đây là integration test bởi nó test hai app layers: ContactsViewController (UI layer) và DataProvider.

Screen Shot 2016-08-20 at 22.38.47.png

Để code compile được, hãy tạo ra class cho DataProvider có mảng ongoingRequests chứa những request đang thực thi và class DataProviderAllContacts thừa kế từ DataProviderProtocol (đại diện cho tất cả các loại request trong app).

Screen Shot 2016-08-20 at 22.43.21.png

Test case có thể thực thi được, tuy nhiên, nó bị fail bởi vẫn chưa có request nào đang thực thi. Để giải quyết vấn đề này, hãy gọi trong ContactViewController:

Screen Shot 2016-08-20 at 22.47.23.png

Và thêm vào method retrieveAllContacts trong DataProvider, phương thức này sẽ add một đối tượng của DataProviderAllContacts vào ongoingRequests array:

Screen Shot 2016-08-20 at 22.55.02.png

Chạy test case và nó pass thành công:

Screen Shot 2016-08-20 at 23.06.03.png

c. Refactor code

Sau khi test case chạy thành công, ta quay lại review code và phát hiện một số issue:

  • Mảng ongingRequests bị lộ ra trong một public interface
  • DataProvider là singleton, singleton sẽ gây phức tạp khi test, bởi nó mang theo trạng thái trong suốt thời gian hoạt động của app.
  • Testcase hiện tại đang là integration test, thay vì đó ta nên viết unit test trong quá trình phát triển bằng việc sử dụng mock DataProvider.

Ẩn chi tiết của implementation:

Chuyển ongoingRequests thành thuộc tính private và cung cấp một pulic methods để lấy nó ra:

Screen Shot 2016-08-20 at 23.22.16.png

Giải quyết singleton:

Singleton giữ lại trạng thái xuyên suốt toàn bộ app, hoặc trong lúc thực thi testing, điều này có thể khiến test case này ảnh hưởng đến test case khác. Thay vào đó ta có thể chỉ tạo ra một singleton duy nhất chứa tham chiếu tới tất cả những class khác có tiềm năng sử dụng singleton.

Hãy tạo mới một class và đặt tên nó là ServiceRegistry, class này sẽ chứa tham chiếu tới DataProvider, vì vậy DataProvider không còn là singleton nữa, việc này cũng giúp mock data dễ dàng hơn.

Screen Shot 2016-08-20 at 23.34.53.png

Tạo mock cho DataProvider:

Để dễ dàng tạo mock, ta tạo ra hai class mới thừa kế từ DataProvider, một cho production (app main target) và một cho testing (test target).

Screen Shot 2016-08-20 at 23.40.19.png

Trong mock class này, nó chỉ chứa một thuộc tính Bool để kiểm tra xem phương thức retrieveAllContacts có được gọi hay không.

Và trong viewWillAppear của ContactViewController:

Screen Shot 2016-08-20 at 23.59.57.png

Class ContactViewControllerTest sẽ được sửa thành:

Screen Shot 2016-08-20 at 23.46.20.png

3. Kết luận

Mục đích của việc viết test trước khi bắt tay vào code là giúp người lập trình nghĩ về vấn đề hơn là giải pháp. Nó bắt buộc bạn phải viết code có thể test dễ dàng, những phương thức lớn thường khó test và bảo trì. Khi code đã thỏa mãn được các test case, nó sẽ trở nên gọn gàng, súc tích và dễ đọc hơn rất nhiều. Việc tạo ra những test case theo đúng thiết kế ban đầu giúp ứng dụng đáp ứng đúng những yêu cầu đã được đặt ra.

Để việc viết code trở nên dễ dàng hơn, thực hiện đúng các chức năng, cũng như tạo niềm vui, đam mê khi lập trình, hãy thử áp dụng TDD trong dự án tiếp theo của bạn nhé.

0