Unlimited Plugins, WordPress themes, videos & courses! Unlimited asset downloads! From $16.50/m
Advertisement
  1. Code
  2. PHP
Code

Viết mã thế nào giúp có thể kiểm thử và bảo trì được trong PHP

by
Difficulty:AdvancedLength:MediumLanguages:

Vietnamese (Tiếng Việt) translation by Brian Vu (you can also view the original English article)

Các framework cung cấp 1 công cụ giúp phát triển ứng dụng nhanh chóng, nhưng lại thường để lại các món "nợ kỹ thuật" nhanh như cách chúng giúp bạn tạo ra các tính năng. "Nợ kỹ thuật" được tạo ra khi việc bảo trì không được coi như 1 mục tiêu trọng tâm đối với các lập trình viên. Những thay đổi và việc gỡ lỗi trong tương lai sẽ trở nên tốn kém, do thiếu kiểm thử unit và cấu trúc mã lệnh cho ứng dụng.

Sau đây, chúng ta sẽ tìm hiểu cách tái cấu trúc mã lệnh giúp chúng có khả năng kiểm thử và bảo trì - cũng như tiết kiệm thời gian cho bạn.


Chủ đề của chúng ta sẽ bao gồm (ở mức độ nhẹ nhàng):

  1. DRY
  2. Dependency Injection
  3. Interfaces
  4. Containers
  5. Unit Tests với PHPUnit

Nào, cùng bắt đầu với một vài đoạn mã lệnh ví dụ điển hình. Chúng có thể là 1 model class trong bất kỳ framework nào.

Đoạn mã trên sẽ chạy, nhưng như vậy là chưa đủ bởi:

  1. Nó không có khả năng kiểm thử
    • Chúng ta đang dựa vào biến toàn cục $_SESSION. Các unit-test framework, như PHPUnit, chạy trên trên môi trường command-line, nơi rất nhiều biến toàn cục không tồn tại.
    • Chúng ta đang dựa vào việc kết nối cơ sở dữ liệu. Lý tưởng nhất là chúng ta nên tránh việc kết nối cơ sở dữ liệu thật trong unit-test. Kiểm thử là việc kiểm tra mã lệnh, chứ không phải dữ liệu.
  2. Đoạn mã này cũng không có tính bảo trì. Ví dụ, nếu chúng ta thay đổi nguồn dữ liệu, chúng ta sẽ cần thay đổi nhân cơ sở dữ liệu trong mỗi thể hiện của App::db được sử dụng trong ứng dụng của chúng ta. Ngoài ra, có trường hợp chúng ta không chỉ muốn mỗi thông tin user hiện tại thì sao?

Thử Xây Dựng Unit Test

Dưới đây là việc cố gắng tạo ra 1 unit test cho chức năng trên.

Cùng phân tích nó, bạn sẽ thấy. Đầu tiên, việc kiểm thử sẽ thất bại. Biến $_SESSION được sử dụng trong đối tượng User không tồn tại trong unit test, cũng như trong PHP command line.

Thứ hai, không có kết nối cơ sở dữ liệu nào được thiết lập. Điều này có nghĩa là, để nó có thể hoạt động, chúng ta sẽ cần can thiệp vào quá trình nạp mồi bootstrap của ứng dụng để lấy đối tượng App và đối tượng ob của nó. Nhớ là chúng ta cũng sẽ cần một kết nối cơ sở dữ liệu hoạt động được.

Để làm đoạn unit test trên chạy được, chúng ta sẽ cần:

  1. Cấu hình 1 PHPUnit CLI chạy trong ứng dụng của chúng ta
  2. Lệ thuộc vào kết nối cơ sở dữ liệu. Việc này đồng nghĩa với lệ thuộc vào 1 nguồn dữ liệu riêng biệt với unit test của chúng ta. Điều gì sẽ xảy ra nếu cơ sở dữ liệu dùng để kiểm thử không có dữ liệu chúng ta mong muốn? Sẽ ra sao nếu kết nối cơ sở dữ liệu của chúng ta chậm chạp?
  3. Dựa vào việc nạp mồi bootstrap chương trình sẽ làm tăng chi phí cho quá trình kiểm thử, làm chậm unit test một cách đáng kể. Lý tưởng nhất, hầu hết mã lệnh của chúng ta nên được kiểm thử độc lập với framework đang sử dụng.

Do đó, hãy kéo xuống để tìm hiểu làm thế nào chúng ta có thể cải thiện việc này.


Giữ cho mã DRY

Hàm getCurrentUser không thực sự cần thiết trong trường hợp đơn giản này. Dưới đây là 1 ví dụ thể hiện tinh thần nguyên tắc DRY, sự tối ưu hoá đầu tiên tôi chọn để thực hiện là khái quát hoá phương thức này.

Điều này cung cấp cho ta một phương thức có thể sử dụng xuyên suốt ứng dụng. Chúng ta có thể truyền current user vào tại thời điểm gọi hàm, hơn là gán cứng 1 phương thức trong model. Mã lệnh trở nên module hoá và khả dụng cho việc bảo trì hơn khi nó không phụ thuộc vào các tính năng khác (vd như biến cục bộ session).

Tuy nhiên, việc này vẫn chưa làm cho mã lệnh có thể kiểm thử và bảo trì được. Chúng vẫn còn lệ thuộc vào việc kết nối cơ sở dữ liệu.


Dependency Injection

Để cải thiện tình hình này chúng ta thêm vào một vài Dependency Injection. Chúng ta sẽ truyền kết nối cơ sở dữ liệu vào class, khi đó model của chúng ta có thể trông như sau.

Tại thời điểm này, các dependency của model User đã được cung cấp. Class của chúng ta không còn sử dụng 1 kết nối cơ sở dữ liệu cụ thể, cũng như không lệ thuộc vào bất kỳ đối tượng toàn cục nào.

Bây giờ, class của chúng ta cơ bản đã khả dụng cho kiểm thử. Chúng ta có thể truyền vào 1 CSDL tuỳ chọn và 1 user id, và kiểm tra kết quả trả về của phương thức được gọi. Chúng ta cũng có thể dễ dàng chuyển đổi các kết nối cơ sở dữ liệu riêng biệt (giả định rằng cả 2 csdl đều thực thi cùng các phương thức nhận dữ liệu). Tuyệt vời phải không nào.

Cùng xem unit test cho đoạn mã trên trông như thế nào nhé.

Tôi đã thêm vào một vài điều mới mẻ trong unit test này: Mockey. Mockey cho phét bạn "mô phỏng" (mock) các đối tượng PHP (giả). Ở trường hợp này, chúng ta đang giả lập kết nối cơ sở dữ liệu. Với việc giả lập này, chúng ta có thể bỏ qua việc kiểm tra kết nối cơ sở dữ liệu và chỉ việc đơn giản thực hiện kiểm thử model của chúng ta. 

Bạn có ý định tìm hiểu thêm về Mockey?

Ở đây, chúng ta đang mô phỏng 1 kết nối SQL. Chúng ta đang nói với đối tượng mô phỏng (mock object) rằng chúng ta muốn có các phương thức select, where, limitget có thể gọi được bên trong nó. Tôi đang trả về thực thể Mock, để ánh xạ cách 1 đối tượng 'kết nối SQL' trả về chính nó ($this), từ đó làm cho phương thức của nó có thể 'kết nối'. Chú ý rằng, đối với phương thức get, tôi trả về kết quả dữ liệu được gọi - là một đối tượng stdClass với dữ liệu user được gắn vào.

Việc này giải quyết một số vấn đề sau:

  1. Chúng ta chỉ kiểm tra model class. Chúng ta không bao gồm kiểm tra kết nối cơ sở dữ liệu.
  2. Chúng ta có thể kiểm soát đầu vào và đầu ra của kết nối cơ sở dữ liệu giả lập, và, vì thế, có thể tin cậy vào kết quả của cơ sở dữ liệu được gọi khi thực hiện kiểm thử. Tôi biết tôi sẽ nhận được kết quả user ID là "1" từ việc truy vấn dữ liệu mô phỏng.
  3. Chúng ta không cần gọi ứng dụng nào trong bootstrap, không cần có bất kỳ cấu hình nào hoặc phải sử dụng cơ sở dữ liệu hiện hành, để phục vụ việc kiểm thử.

Thậm chí có thể làm tốt hơn. Dưới đây bạn sẽ thấy sự thú vị này.


Interfaces

Để cải tiến hơn nữa, chúng ta có thể định nghĩa và thực thi một interface. Cùng xem đoạn mã dưới đây.

Có một vài thứ xảy ra ở đây.

  1. Đầu tiên, chúng ta định nghĩa một interface cho nguồn dữ liệu user của chúng ta. Interface này định nghĩa phương thức getUser().
  2. Tiếp theo, chúng ta thực thi interface này. Ở đây, chúng ta tạo ra 1 hệ thống thực thi MySQL. Chúng ta chấp nhận một đối tượng kết nối cơ sở dữ liệu, và sử dụng nó để lấy user từ cơ sở dữ liệu.
  3. Cuối cùng, chúng ta ràng buộc việc sử dụng class thực thi UserInterface trong model User của chúng ta. Việc này đảm bảo rằng nguồn dữ liệu sẽ luôn có 1 phương thức getUser() tồn tại, không cần biết nguồn dữ liệu nào thực thi UserInterface.

Chú ý rằng model User sử dụng object type-hints UserInterface trong constructor của nó. Điều này có nghĩa là bắt buộc PHẢI truyền vào đối tượng  User 1 class thực thi UserInterface. Việc này bảo đảm cho cái chúng ta đang dựa vào - chúng ta cần phương thức getUser luôn tồn tại.

Kết quả của của việc này là gì ?

  • Mã lệnh của chúng ta giờ đây hoàn toàn có thể kiểm thử. Đối với class User, chúng ta có thể dẽ dàng mô phỏng nguồn dữ liệu. (Kiểm tra hệ thống thực thi của nguồn dữ liệu sẽ là công việc tách biệt với unit test)
  • Mã của chúng ta mang tính bảo trì mạnh mẽ hơn. Chúng ta có thể chuyển đổi các nguồn dữ liệu khác nhau mà không phải thay đổi mã lệnh, xuyên suốt ứng dụng của chúng ta.
  • Chúng ta có thể tạo BẤT KỲ nguồn dữ liệu nào. ArrayUser, MongoDbUser, CouchDbUser, MemoryUser, v...v...
  • Chúng ta có thể dễ dàng truyền bất kỳ nguồn dữ liệu nào vào đối tượng User khi chúng ta cần. Nếu bạn không muốn dùng SQL, bạn chỉ cần tạo 1 hệ thống thực thi khác (ví dụ như MongoDbUser) và truyền nó vào trong model User.

Chúng ta đã đơn giản hoá việc kiểm thử unit!

Chúng ta đã tách công đoạn mô phỏng việc kết nối dữ liệu ra riêng hoàn toàn. Thay vào đó, chúng ta chỉ đơn giản mô phỏng nguồn dữ liêu, và giao việc cho nó khi getUser được gọi.

Nhưng, chúng ta thậm chí có thể làm tuyệt vời hơn nữa!


Containers

Cùng xem xét việc sử dụng đoạn mã bên dưới:

Cuối cùng, tôi muốn giới thiệu với các bạn về containers. Trong đoạn mã trên, chúng ta phải tạo và dùng nhiều đối tượng chỉ để lấy current user. Và mã lệnh này có thể được sử dụng lại nhiều lần trong ứng dụng của bạn. Nếu bạn muốn chuyển từ MySQL qua MongoDB, bạn sẽ cần chỉnh sửa ở nhiều nơi có đoạn mã trên. Điều này phá vỡ nguyên lý DRY. Containers có thể giải quyết vấn đề này.

Một container sẽ "chứa" một object hoặc một tính năng. Nó tương tự như 1 registry trong ứng dụng của bạn. Chúng ta có thể sử dụng 1 container để tự động khóa việc khởi tạo đối tượng User với mọi dependencies cần thiết. Dưới đây, tôi sử dụng Pimple, một container class khá phổ biến.

Tôi đã chuyển tiến trình tạo model User vào trong khu vực cấu hình của ứng dụng. Và kết qủa là:

  1. Chúng ta đã giữ cho mã lệnh DRY. Đối tượng User và tùy chọn lưu trữ dữ liệu được định nghĩa trong cùng 1 nơi trong ứng dụng của chúng ta.
  2. Chúng ta có thể chuyển đổi việc sử dụng nguồn dữ liệu như MySQL hay bất kỳ nguồn nào khác cho model User trong cùng MỘT nơi. Việc này tăng tính bảo trì cho mã lệnh rất nhiều.

Kết Luận

Xuyên suốt toàn bộ bài viết này, chúng ta đã gặt hái được:

  1. Việc giữ cho mã DRY và khả năng sử dụng lại cao
  2. Xây dựng các đoạn mã có khả năng bảo trì - Chúng ta có thể chuyển đổi qua lại các nguồn dữ liệu cho các đối tượng trong cùng một nơi cho toàn bộ ứng dụng nếu cần.
  3. Làm cho mã lệnh của chúng ta có khả năng kiểm thử - Chúng ta có thể dễ dàng mô phỏng các đối tượng mà không cần dựa vào việc nạp mồi bootstrap cho ứng dụng hoặc tạo ra 1 cơ sở dữ liệu kiểm thử.
  4. Học cách sử dụng Dependency Injection và Interfaces, cho phép chúng ta tạo ra các đoạn mã mang tính khả dụng cho việc kiểm thử và bảo trì.
  5. Thấy được cách containers có thể giúp đỡ việc xây dựng 1 ứng dụng dễ bảo trì hơn.

Chắc bạn cũng để ý thấy là chúng ta đã thêm vào nhiều mã lệnh hơn để mang lại khả năng bảo trì và kiểm thử. Một lập luận mạnh mẽ có thể chống lại quan điểm này là: chúng ta đang làm tăng sự phức tạp lên. Thực tế thì những vấn đề này sẽ đòi hỏi sự hiểu biết chuyên sâu về lập trình, đối với cả người nắm chính dự án và các thành viên làm việc trong cùng 1 dự án.

Tuy nhiên, cái giá cho sự nghiên cứu và hiểu biết thì không đáng kể bởi nó giúp chúng ta giảm đi món "nợ kỹ thuật" rất nhiều.

  • Mã lệnh cực kỳ dễ bảo trì, việc chỉnh sửa các thay đổi chỉ cần làm ở 1 nơi, thay vì phải thực hiện ở nhiều chỗ.
  • Có thể nhanh chóng xây dựng unit test, sẽ giảm lỗi trong mã lệnh - đặc biệt là về lâu dài hoặc các dự án mang tính cộng đồng (open-source).
  • Kỹ lưỡng tại thời điểm ban đầu sẽ có nhiều việc phải làm, nhưng sẽ giúp chúng ta tiết kiệm thời gian và những vấn đề nan giải, nhức đầu về sau này.

Nguồn tài nguyên

Bận có thể dễ dàng đưa MockeyPHPUnit vào trong ứng dụng của bạn bằng cách sử dụng Composer. Thêm chúng vào mục "require-dev" trong file composer.json:

Sau đó, bạn sẽ có thể cài đặt các dependencies của Composer với tùy chọn yêu cầu "dev" như bên dưới.

Bạn có thể tìm hiểu thêm về Mockey, Composer và PHPUnit trên Nettuts+ tại đây.

Đối với PHP, bạn nên cân nhắc sử dụng Laravel 4, vì nó có sử dụng containers, cũng như các ý tưởng khác được viết ở trên.

Cám ơn bạn đã đọc bài viết này!

Advertisement
Advertisement
Advertisement
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.