Khi nói về software testing, chúng ta thường bắt đầu với một câu hỏi khá đơn giản: hệ thống có hoạt động đúng không? Đó là phạm vi quen thuộc của functional testing. Chúng ta kiểm tra một function có trả về đúng kết quả hay không, một API có phản hồi đúng hay không, hoặc một user có thể hoàn thành một flow hay không.
Nhưng một hệ thống có thể hoàn toàn đúng về mặt chức năng mà vẫn thất bại khi chạy trong production. Nó có thể chạy rất nhanh với vài trăm bản ghi nhưng trở nên chậm chạp khi database chứa hàng triệu bản ghi. Một service có thể hoạt động bình thường trong vài phút nhưng sau nhiều giờ lại gặp memory leak. Một hệ thống có thể xử lý tốt traffic bình thường nhưng sụp đổ khi lượng request tăng đột biến. Một dependency bị down có thể khiến cả hệ thống ngừng hoạt động, dù bản thân business logic không có vấn đề gì.
Đó là lúc non-functional testing trở nên quan trọng. Nếu functional testing tập trung vào câu hỏi “hệ thống có làm đúng việc cần làm không?”, thì non-functional testing quan tâm nhiều hơn đến việc “hệ thống làm việc đó như thế nào trong những điều kiện thực tế?” Một số nhóm quan trọng của non-functional testing bao gồm performance, data volume, stability, reliability, stress and scalability, và security testing.
Performance testing
Performance testing tập trung vào việc đo lường và bảo vệ hiệu năng của hệ thống. Ở mức đơn giản nhất, chúng ta có thể đo một đoạn code quan trọng về performance. Ví dụ, một data processing service có một vòng lặp liên tục thực hiện việc decode, transform và encode message. Thay vì chỉ kiểm tra rằng logic này cho ra kết quả đúng, chúng ta có thể chạy nó hàng chục nghìn lần và đo thời gian thực thi.
Kết quả đo được có thể trở thành một baseline. Sau này, nếu một developer thay đổi implementation và performance giảm đáng kể, performance test có thể phát hiện ra regression đó. Ngược lại, khi thực hiện optimization, chúng ta cũng có thể dùng cùng baseline để kiểm tra xem optimization thực sự mang lại cải thiện hay chỉ đơn giản là cảm giác “có vẻ nhanh hơn”.
Điều quan trọng ở đây là performance không nên chỉ được đánh giá bằng cảm giác. Một thay đổi nhỏ trong code có thể không làm thay đổi functional behavior nhưng lại khiến một đoạn xử lý chạy chậm hơn đáng kể. Nếu không có measurement và baseline, những regression như vậy rất dễ lọt qua code review và chỉ được phát hiện khi hệ thống đã chạy trong production.
Performance testing cũng không chỉ giới hạn ở một function. Chúng ta có thể đo performance của một component, một service, hoặc thậm chí toàn bộ flow end-to-end. Ví dụ, thay vì đo riêng một function xử lý message, chúng ta có thể đo thời gian một service cần để xử lý một triệu message, hoặc đo toàn bộ quá trình từ data ingestion đến data export.
Tuy nhiên, performance test cần một môi trường tương đối ổn định. Một threshold hợp lý trên máy của developer này có thể không hợp lý trên máy khác. Tương tự, nếu CI chạy trên những loại machine khác nhau, kết quả benchmark cũng có thể dao động. Vì vậy, baseline và execution environment cần được kiểm soát đủ tốt để kết quả có ý nghĩa.
Data volume testing
Performance không chỉ thay đổi theo code. Nó còn thay đổi theo lượng dữ liệu. Một query chạy rất nhanh trên database gần như trống có thể trở nên chậm khi database chứa hàng triệu record. Index, table size và lượng dữ liệu cần xử lý đều có thể ảnh hưởng đến performance.
Data volume testing tập trung vào việc kiểm tra tác động này. Thay vì chỉ test với một database nhỏ giống môi trường development, chúng ta đưa vào một lượng dữ liệu đủ lớn và quan sát hệ thống thay đổi như thế nào.
Đây là một vấn đề khá phổ biến: hệ thống hoạt động hoàn hảo trong development không có nghĩa là nó sẽ giữ nguyên performance khi dữ liệu tăng lên trong production.
Stability testing
Có một loại bug không xuất hiện ngay lập tức. Một service có thể chạy tốt trong mười phút, một giờ, thậm chí vài giờ, nhưng sau đó bắt đầu tiêu thụ ngày càng nhiều memory hoặc xuất hiện những lỗi rất khó tái hiện. Đó là vấn đề mà stability testing hướng tới.
Thay vì chạy hệ thống trong một khoảng thời gian ngắn, chúng ta để hệ thống chạy liên tục dưới load trong nhiều giờ, thậm chí nhiều ngày. Loại testing này còn được gọi là endurance, load hoặc soak testing. Mục tiêu là tìm ra những vấn đề chỉ xuất hiện theo thời gian, chẳng hạn memory leak, resource exhaustion hoặc những sporadic bug chỉ xảy ra trong một số điều kiện nhất định.
Load trong quá trình test cũng nên gần với production càng nhiều càng tốt. Production traffic hiếm khi là một đường thẳng đều đặn — thường sẽ có peak và low. Việc mô phỏng một pattern tự nhiên như vậy có thể giúp phát hiện những vấn đề mà một bài test với constant load không tìm thấy.
Reliability testing
Nếu stability testing hỏi “hệ thống có thể chạy ổn định trong thời gian dài không?”, thì reliability testing đặt một câu hỏi khác:
“Điều gì xảy ra khi một thứ bị hỏng?”
Trong một distributed system, failure là điều không thể tránh khỏi. Network có thể mất kết nối, server có thể crash, database có thể unavailable, một node có thể biến mất hoặc một external service có thể ngừng hoạt động. Một hệ thống đáng tin cậy không nhất thiết là hệ thống không bao giờ fail; quan trọng hơn là khi failure xảy ra, hệ thống có thể chịu đựng failure và tự phục hồi như thế nào.
Đây là lý do reliability testing thường gắn với chaos engineering. Chẳng hạn, một service chạy trên Kubernetes có nhiều pod. Nếu một node bị down và một pod biến mất, service vẫn phải tiếp tục hoạt động thông qua những pod còn lại. Sau đó Kubernetes cần phát hiện pod bị mất và tạo một pod mới để khôi phục trạng thái ban đầu. Thay vì chỉ tin rằng configuration này sẽ hoạt động khi production gặp sự cố, chúng ta có thể chủ động tạo ra failure và kiểm tra behavior thực tế của hệ thống.
Tương tự, nếu service phụ thuộc vào Kafka, chúng ta có thể chủ động shutdown Kafka và quan sát hệ thống:
- Service có phát hiện dependency đã unavailable không?
- Alert có được trigger không?
- Khi Kafka hoạt động trở lại, alert có tự động biến mất không?
- Service có tiếp tục hoạt động bình thường không?
Những câu hỏi này khó trả lời chỉ bằng functional testing.
Stress và scalability testing
Một hệ thống có thể hoạt động tốt dưới normal load, nhưng điều gì xảy ra khi load tăng lên gấp nhiều lần? Đó là phạm vi của stress testing.
Trong stress testing, hệ thống được đặt dưới mức tải cao hơn mức thông thường. Mục tiêu không chỉ là xem hệ thống có crash hay không, mà còn để hiểu hệ thống phản ứng thế nào khi demand tăng lên và khả năng scale của nó ra sao.
Đối với một hệ thống có khả năng horizontal scaling, chúng ta có thể kiểm tra cả quá trình scale out và scale in. Khi load tăng, hệ thống có tạo thêm instance không? Khi load giảm, những instance không cần thiết có được loại bỏ không?
Metric dùng để trigger scaling cũng không nhất thiết phải là CPU hoặc memory. Với một Kafka consumer, chẳng hạn, consumer lag có thể là một signal phù hợp hơn. Khi lag tăng quá cao, hệ thống có thể scale out để tăng processing capacity. Khi lag giảm xuống, hệ thống có thể scale in trở lại.
Điều này cho thấy scalability không đơn giản là “server mạnh hơn”. Một hệ thống scalable cần có behavior phù hợp khi workload thay đổi.
Security testing
Cuối cùng là security testing. Một hệ thống có thể đúng về chức năng, nhanh, ổn định và có khả năng scale, nhưng vẫn không thể xem là một hệ thống tốt nếu nó có những lỗ hổng bảo mật nghiêm trọng.
Security testing bao gồm nhiều hoạt động khác nhau. Một phần là tự động scan các software artifact, chẳng hạn container image, để tìm những vulnerability đã biết. Một phần khác là penetration testing, trong đó hệ thống được kiểm tra theo cách gần với một cuộc tấn công thực tế hơn. Các bài test có thể tìm kiếm những vấn đề như SQL injection, cross-site scripting, information disclosure, denial of service, code execution, CSRF hoặc các dạng vulnerability khác.
Security cũng là một lĩnh vực mà automation đặc biệt hữu ích. Một số vấn đề có thể được phát hiện sớm trong CI/CD thay vì đợi đến khi hệ thống đã được deploy.
Và còn nhiều thứ khác
Non-functional testing không chỉ dừng lại ở performance, reliability hay security. Còn có những khía cạnh gần với trải nghiệm người dùng hơn như accessibility, usability, localization, internationalization và visual testing.
Visual testing là một ví dụ khá thú vị. Một thay đổi HTML hoặc CSS có thể không làm hỏng bất kỳ business logic nào. Functional tests vẫn pass. API vẫn trả về đúng response. Nhưng UI có thể bị lệch layout hoặc một component nào đó bị thay đổi ngoài ý muốn. Snapshot testing có thể giúp phát hiện những thay đổi như vậy bằng cách chụp lại UI và so sánh với một snapshot đã biết.
Điểm quan trọng nhất của non-functional testing, theo mình, là nó mở rộng cách chúng ta định nghĩa “software works”. Một hệ thống không chỉ cần trả về đúng kết quả. Nó cần đủ nhanh khi workload tăng. Cần tiếp tục hoạt động khi dữ liệu lớn lên. Cần ổn định khi chạy trong thời gian dài. Cần chịu được failure và có khả năng recovery. Cần scale khi traffic tăng. Và cần đủ an toàn trước những cách sử dụng hoặc tấn công mà chúng ta không mong muốn.
Functional testing giúp chúng ta hỏi “hệ thống có làm đúng không?”. Non-functional testing buộc chúng ta hỏi thêm:
“Nó có tiếp tục làm đúng khi môi trường xung quanh nó thay đổi không?”
Và trong một production system thực sự, câu hỏi thứ hai đôi khi mới là phần khó hơn.