Trong bài viết này mình sẽ đề cập đến 7 Test Design Techniques mà mình thấy là phổ biến nhất và cần thiết cho một tester khi viết test case.

Cho Blackbox Testing :

Cho Whitebox Testing:

EP - Equivalent Partitioning

Equivalent Partitioning sẽ áp dụng cho các tính năng liên quan đến input/dữ liệu đầu vào, khi mà yêu cầu tính năng liên quan đến input nêu ra hành vi của hệ thống sẽ dựa vào giá trị của input

Định nghĩa đầy đủ của nó là một design technique nhằm giải quyết việc phân chia test case cho các yêu cầu liên quan đến vùng của giá trị, khi mà kết quả của hệ thống phụ thuộc cụ thể vào giá trị của input. Technique này sẽ tạo ra các phân vùng tương đương - equivalent partition, đặc điểm của phân vùng tương đương là các tập hợp giá trị input mà kết quả đầu ra giống nhau, giống output. Khi thực hiện kiểm thử, vì kết quả đầu ra giống nhau nên tester không thực sự cần thực hiện kiểm thử trên toàn bộ các giá trị của từng phân vùng mà chỉ cần chọn giá trị đại diện cho mỗi phân vùng.

Ví dụ, một chức năng yêu cầu nhập tuổi của người dùng và từ đấy phân loại người dùng thành 4 nhóm trẻ em, vị thành niên, người trưởng thành, người già.

Với một yêu cầu như bên trên, cách chia test case chính là áp dụng EP, quy luật áp dụng như sau:

Đầu tiên, chúng ta sẽ chia thành 5 phân vùng tương đương: Bé hơn 0 là phân vùng không hợp lệ, từ 0 đến nhỏ hơn 13, từ 13 đến nhỏ hơn 19, từ 19 đến nhỏ hơn 61, cuối cùng là từ 61 trở lên

----0----12----18----60----

Sau đó, lấy ra giá trị đại diện cho mỗi phân vùng: -1 cho phân vùng không hợp lệ, 6 cho trẻ em, 15 cho vị thành niên, 40 cho người trưởng thành, 70 cho người già

---(-1)-0--6--12--15--18--40--60--70--
     |     |      |        |       |

Sau đó, chúng ta có thể tạo 5 High Level Test Case cho yêu cầu này:

OK, đấy chính là EP, có bao nhiêu phân vùng tương đương, thì có bấy nhiêu test case, bao gồm cả các phân vùng không hợp lệ và hợp lệ.

BVA - Boundary Value Analysis

BVA - Boundary Value Analysis là một Test Design Technique sinh ra để bổ trợ cho EP. Vấn đề sẽ phát sinh như sau:

Dev sẽ cần phải xử lý và đưa yêu cầu trên vào codebase của sản phẩm. Trong các dòng code, họ sẽ cần sử dụng các operator để so sánh giá trị biến input với giá trị biên của các vùng giá trị, vậy thì

Nếu như họ làm sai, thay vì tuổi trẻ em là 0<age<=12 thì họ lại làm 0<=age<12 thì sao, khi đấy giá trị input là 12, hành vi của hệ thống sẽ không giống như mong muốn. Kiểm thử sử dụng EP thường xuyên bỏ qua các giá trị biên, kể cả khi tester cố tình chọn 12 là giá trị đại diện, dev mà làm sai nốt 0<age<12 thì chỉ 12 sẽ không đủ mà cần giá trị 0. Đó chính là lý do BVA ra đời.

Logic tóm lại ở đây là, bugs sẽ thường xuyên xuất hiện ở các giá trị biên, do có thể có sai sót khi sử dụng các operator so sánh, nên BVA sẽ được kết hợp cùng với EP để chia test case, cover toàn bộ các tình huống có thể xảy ra.

BVA sẽ có 2 phương pháp chia test case cho mỗi giá trị biên:

Ví dụ, cho giá trị biên 0, mình thường sẽ áp dụng 2 value testing, chia thành 2 test case: -1 và 0. Cho giá trị 12, mình thường sẽ áp dụng 3 value testing, chia thành 3 test case: 11, 12, 13. Lý do là vì 12 là điểm biên ở giữa 2 phân vùng hợp lệ thế nên cần check điểm biên và check 2 giá trị 2 bên ở 2 phân vùng.

Vậy thì ví dụ ở trên sẽ nhìn thế nào với BVA + EP:

---(-1)-0--11-12-13--17-18-19---60-61---
     |  |   |  |  |   |  |  |   |  |

Sau khi áp dụng BVA, chúng ta sẽ có các test case sau:

Tại đây, bạn có thể lập luận rằng, rõ ràng có 3 giá trị 0,11,12 cho cùng kết quả, vậy thì có thể loại bỏ test case 11, tương tự với 13,17,18 , có thể loại bỏ test case 17. Việc này hoàn toàn là có thể, tuy nhiên nó phụ thuộc vào cụ thể bài toán. Việc này chính là giản hoá test case, giúp giảm số lượng test case cần viết.

Decision Table

Như ở trên đã có 2 designing technique áp dụng cho các yêu cầu liên quan đến vùng giá trị đầu vào, vậy thì nếu như chúng ta có nhiều hơn 1 input thì phải làm thế nào? Giống như một số bài toán xác suất khi đi học, nếu A có thể có 2 giá trị, B có thể có 3 giá tị, vậy tổ hợp của A và B sẽ có 6 giá trị, đấy chính là bản chất của Decision Table.

Decision Table là một Test Design Technique áp dụng cho các yêu cầu liên quan đến nhiều input, khi mà kết quả đầu ra của hệ thống phụ thuộc vào cụ thể tổ hợp giá trị của các input. Technique này sẽ tạo ra một cái gọi là bảng quyết định (Decision Table) để giúp chia test case.

Đầu tiên, bảng quyết định hay còn gọi là bảng Conditions/Actions, sẽ chia làm 2 phần, phần Conditions và phần Actions. Phần Conditions sẽ chứa các input và phần Actions sẽ chứa các output tương ứng với từng bộ input.

Template bảng Decision Table
Template bảng Decision Table

Template bảng Decision Table

Như đã biết, Condition ở đây chính là các Input, vậy thì hiểu nó thế nào?

Chúng ta trước mắt sẽ có 4 tổ hợp : Correct usernameCorrect password, Correct usernameIncorrect password, Incorrect usernameCorrect password, Incorrect usernameIncorrect password

Ví dụ, chúng ta sẽ có 2 input là usernamepassword, chúng ta sẽ có bảng quyết định trước khi điền Action như sau:

Decision Table với Conditions
Decision Table với Conditions

Decision Table với Conditions

Tiếp theo, chúng ta điền vào phần Actions các kết quả mong muốn với từng bộ Input, Login với bộ input đầu tiên và Not Login với 3 bộ input còn lại

Decision Table với Conditions và Actions
Decision Table với Conditions và Actions

Decision Table với Conditions và Actions

Vậy gần như đã xong, vấn đề còn lại chính là giản hoá bảng quyết định, loại bỏ các bộ input không cần thiết, chúng ta sẽ cần quan tâm đến 2 yếu tố:

Decision Table sau giản hoá
Decision Table sau giản hoá

Decision Table sau giản hoá

Mặc dù việc chia test case theo tổ hợp là hợp logic để cover toàn bộ khả năng có thể xảy ra, nhưng số lượng test case sinh ra là cấp số nhân, vậy khâu giản hoá (simplification) luôn cần thiết để tránh phải sinh ra các test case dư thừa.

Chốt lại chúng ta sẽ có 3 test case, sau khi đã lên ý tưởng với Decision Table thì chúng ta sẽ nắm được đại ý 3 test case cần viết ra như sau:

Giờ để tạo nốt High Level Test Case thì cho nó một giá trị cho username và password, High Level Test Case phải cụ thể

State Transitioning

State Transitioning là một Test Design Technique áp dụng cho các yêu cầu liên quan đến workflow, useflow, tức là người dùng sẽ thực hiện một loạt thao tác và trạng thái của một đối tượng cụ thể sẽ thay đổi sau mỗi một thao tác. Theo lý thuyết thì nó phù hợp nhất với một chuỗi các sự kiện và điều kiện, khi mà hoạt động của hệ thống với các sự kiện và điều kiện này phụ thuộc vào sự kiện và điều kiện trong quá khứ.

Ví dụ, hiểu nôm na nếu chúng ta có một flow đăng nhập, người dùng đăng nhập vào hệ thống, người dùng vào cài đặt tài khoản, người dùng yêu cầu vô hiệu hoá tài khoản, sau 1 tuần nếu người dùng không hồi phục thì tài khoản sẽ bị xoá hoàn toàn, thì đối tượng trải qua thay đổi trạng thái ở đây chính là tài khoản của người dùng, và các trạng thái nó trải qua chính là Active, Inactive, Deleted. Các dev sẽ phải thực tế hoá yêu cầu này, và khi test, các tester sẽ áp dụng State Transitioning để chia test case.

Cách chia test case như sau:

Sau đó dựa vào Diagram, chúng ta sẽ viết test case cho từng thay đổi trạng thái của đối tượng.

Mình sẽ cho một ví dụ như ở trên nhưng đưa thêm một số chi tiết để bạn hiểu rõ hơn:

Người dùng có thể tạo một tài khoản. Tài khoản này nếu kể từ thời điểm tạo ra, sau 6 tháng không có giao dịch sẽ tự động bị vô hiệu hoá, trong 6 tháng tiếp theo nếu không được hồi phục sẽ bị xoá hoàn toàn.

Tài khoản nếu vi phạm các điều khoản sử dụng, tuỳ theo mức độ nghiêm trọng , thấp và cao, có thể bị vô hiệu hoá hoặc xoá hoàn toàn ngay lập tức.

Người dùng có thể vào cài đặt tài khoản để vô hiệu hoá tài khoản, sau khi vô hiệu hoá, tài khoản sẽ chuyển sang trạng thái Inactive, sau 6 tháng nếu không hồi phục sẽ bị xoá hoàn toàn

OK, với yêu cầu trên, đầu tiên chúng ta xác định có 3 trạng thái: Active, Inactive, Deleted

6 months no transactionUser deactivate accountViolate low term6 months no recoveryViolate high termActiveInactiveDeleted
// Mermaid diagram

Dựa vào đây, chúng ta sẽ có 5 test case cho 5 trạng thái chuyển đổi:

Và đó chính là State Transitioning

Statement Testing

Statement Testing hay còn gọi là Statement Coverage là một Test Design Technique áp dụng cho các tình huống cần Whitebox Testing. Tức là chúng ta đang test sản phẩm làm việc khi mà chúng ta cần quan tâm đến cấu trúc code, vì test code nên Test Design Technique này áp dụng với Unit Test và Integration Test là nhiều. Với Statement Testing thì mục tiêu của việc chia test case chính là làm thế nào để mỗi executable statement được chạy ít nhất một lần.

Ví dụ, có một block if-else trong Python như sau:

if a > 0:
    print('a is positive')
else:
    print('a is negative')

Để mọi dòng code có thể được chạy, trong ví dụ trên chúng ta cần tạo test case để block if và block else chạy, đương nhiên chúng nó không thể chạy cùng lúc nên cần 2 test case, một test case với a > 0 và một test case với a <= 0. Với kiến thức Python thì chúng ta cũng thấy việc handle cả 2 trường hợp a > 0a <= 0 là đủ.

Vậy thì với Statement Testing sẽ có 2 High Level Test Case:

Với giá trị -55 là giá trị đại diện cho 2 phân vùng, ở đây chúng ta cũng đang áp dụng cả EP. Nếu bạn nghĩ mình đang ví dụ thiếu thiếu, cần phải áp dụng cả BVA + EP thì bạn đang nhầm lẫn, mục tiêu của chúng ta là để tất cả các dòng code được chạy và chỉ thế thôi. Chúng ta không phải là đang xác minh tính chính xác của code, mục tiêu của Statement Testing nói riêng hay Whitebox Testing nói chung là để phục vụ cho việc kiểm tra cấu trúc code, xem code có tốt hay không, không phải là kiểm tra logic code.

Để giải thích rõ hơn, hãy nhìn vào ví dụ dưới đây:

if a > 0:
     if a == -1:
         print('a is -1')
     else:
         print('a is positive')
else:
     print('a is negative')

Trong tình huống trên, chúng ta có thể thấy, có một vấn đề với code. Nếu a đã lớn hơn 0 thì làm thế nào mà block if a == -1 có thể chạy? Statement print('a is -1') sẽ không bao giờ có thể được thực thi với mọi giá trị của a, vì thế với mọi tổ hợp test case, sẽ không bao giờ có thể cover đủ 100% số dòng code. Statement Testing có thể giúp chúng ta xác định ra những dòng code không thể nào được thực thi, từ đó giản lược code. Ngoài ra, việc thực hiện statement testing cũng giúp cover các trường hợp code lỗi.

Với test case a is positive with a = 5, sẽ có 3/7 dòng code được thực thi và với a is negative with a = -5 sẽ có 2/7 dòng code được thực thi, tổng hai test case này sẽ chỉ có 5/7 dòng code được thực thi và không thể đạt 7/7, chúng ta sẽ không thể cover được dòng code print('a is -1') với mọi giá trị của a, vậy tức là có một nhánh code dư thừa.

Việc thực hiện Statement Testing thường đi kèm với việc theo dõi tỉ lệ coverage của code, tỉ lệ này thường được đo lường bằng %, coverage giúp nhìn phần code chưa được chạy. Mục tiêu bao nhiêu phần trăm phải do team và rủi ro quyết định; 100% statement coverage vẫn không chứng minh code đúng.

Decision/Branch Testing

Với hiểu biết về Statement Testing bên trên, bạn hãy xác định test case cho đoạn code sau:

if type(a) == int and a > 0:
    print('a is positive')

if type(a) == str and len(a) > 0:
    print('a is not empty')

Với Statement Testing, chúng ta có thể viết 2 test case:

Tuy nhiên, thử nghĩ xem, nếu a là integer và a = -1 thì sẽ thế nào? Như bạn thấy, đống code trên xuất hiện các nhánh kết quả mà Statement Testing không thể cover do không hề có đoạn code nào được viết để xử lý nhánh đó, thế nên bản chất Statement Testing cho đoạn code trên là không đủ.

Chúng ta có một khái niệm khác là Decision hay Branch Testing, tức nghĩa chúng ta sẽ chia test case để tất cả các nhánh kết quả của code có thể được chạy chứ không đơn thuần là tất cả dòng code. Decision Testing sẽ rộng hơn Statement Testing, do đó nếu Statement Testing cùng Statement Coverage đạt 100%, thì chưa chắc Decision Testing cùng Decision Coverage cũng đạt 100%, nhưng ngược lại, nếu Decision Testing đạt 100% thì Statement Testing cũng sẽ đạt 100%.

Với đoạn code trên, chúng ta sẽ cần 4 test case:

Do rằng, với a = -1a = '' thì không có dòng code nào được thực thi, nhưng chúng ta vẫn cần viết test case để cover nhánh kết quả đó vì nó có thể xảy ra lỗi khi điều kiện của các block if không thoả mãn. Với 4 test case trên, đoạn code trên vẫn chạy tốt mà không lỗi, và chúng ta đã cover được tất cả các nhánh kết quả của code và kết luận được đoạn code trên là ổn.

Để ví dụ tính quan trọng của Decision Testing rõ hơn trong việc tìm ra lỗi của code, mình có ví dụ sau:

if a > 0:
     'a is positive'
     global b
     b = 1

if b == 1:
     print('b is 1')

Với đoạn code trên, nếu chỉ thực hiện Statement Testing, chúng ta sẽ cần đúng 1 test case:

Khi đặt a = 5 thì toàn bộ 6 dòng code sẽ đồng thời chạy và Statement Testing thành công với Statement Coverage 100%. Vấn đề ở chỗ, trong tình huống chúng ta áp dụng phản điều kiện của a thì sao? Khi a < 0 thì code định nghĩa của b sẽ không được thực thi, dẫn đến lỗi b is not defined khi chạy dòng code if b == 1. Lý do vì nhánh a < 0 đã không được handle tốt bởi người đi code, và chúng ta cần viết test case để cover nhánh kết quả này.

Và khi thực hiện, test case đầu chắc chắn sẽ fail và chúng ta có thể kết luận là đoạn code trên chưa ổn.

Path Testing

Path Testing là một Test Design Technique, chính là kết hợp của Statement Testing và Decision Testing, tức là chúng ta sẽ chia test case để tất cả các dòng code và tất cả các nhánh kết quả của code có thể được chạy. Path Testing là một trong những Test Design Technique phức tạp nhất, và cũng thường được áp dụng cho Whitebox Testing.

Lý do tại sao lại cần áp dụng cả Statement Testing và Decision Testing vào làm một là để cover các loại code mang tính flow, tức là nó không chỉ đơn thuần là chạy từ trên xuống dưới. Những tình huống code như recursive, loop, ... hoạt động theo điều kiện. Theo kinh nghiệm và cách hiểu của mình, cứ miễn diagram của code có các thành phần như loop, feedback, hay các code liên quan đến nhiều input thì sẽ cần áp dụng Path Testing.

Quan sát ví dụ sau:

def foo():
     if A == 'red':
          print('A is red')
     if B == 'red':
          print('B is red')

Hãy bắt đầu từ Statement Testing trước, chỉ cần viết đúng 1 test case:

Chỉ cần 1 test case này là đủ để chạy hết 4 dòng của function, Statement Testing đạt 100% Statement Coverage

Tiếp theo, vấn đề xảy ra khi AB không phải red thì sao? Vậy cần 2 test case để cover 2 nhánh kết quả cho cả 2 khối if:

Tuy nhiên, nếu nói về Path thì thực tế, nếu nhớ về Decision Table Test Design Technique thì chúng ta sẽ thấy có 4 tổ hợp giá trị cho 2 input AB, vậy thì chúng ta cần 4 test case để cover toàn bộ Path của function foo:

Tóm lại, không cần phải hiểu quá máy móc:

Statement Testing là chia test case để từng dòng code chạy ít nhất 1 lần

Decision Testing là chai test case để mỗi nhánh của một nhát True/False của code chạy ít nhất 1 lần

Path Testing là chia test case để tất cả dòng codetất cả nhánh của code chạy ít nhất 1 lần

Nguồn tham khảo