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 :
- EP - Equivalent Partitioning - Phân vùng tương đương
- BVA - Boundary Value Analysis - Phân tích giá trị biên
- Decision Table - Bảng quyết định
- State Transitioning - Chuyển trạng thái
Cho Whitebox Testing:
- Statement Testing
- Decision/Branch Testing
- Path 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à.
- Tuổi từ 0 đến 12 là trẻ em
- Tuổi từ 13 đến 18 là vị thành niên
- Tuổi từ 19 đến 60 là người trưởng thành
- Tuổi từ 61 trở lên là 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:
- Dựa trên đặc điểm trong sự khác biệt về đầu vào và đầu ra, chia thành các phân vùng tương đương
- Chọn giá trị đại diện cho mỗi phân vùng, thường sẽ là một giá trị ở giữa, như trung bình, để tránh bias
Đầ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:
User can't input age -1User is identified as a child when age is 6User is identified as a teenager when age is 15User is identified as an adult when age is 40User is identified as an elder when age is 70
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:
- 2 giá trị: Chia 2 test case : một giá trị ở gần giá trị biên ở phía phân vùng không hợp lệ, giá trị còn lại là giá trị biên
- 3 giá trị: Chia 3 test case : một giá trị ở gần giá trị biên ở phía phân vùng không hợp lệ (hoặc phân vùng bên trái), giá trị biên, giá trị ở gần giá trị biên ở phía phân vùng bên phải
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:
User can't input age -1User is identified as a child when age is 0User is identified as a child when age is 11User is identified as a child when age is 12User is identified as a teenager when age is 13User is identified as a teenager when age is 17User is identified as a teenager when age is 18User is identified as an adult when age is 19User is identified as an adult when age is 60User is identified as an elder when age is 61
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
Như đã biết, Condition ở đây chính là các Input, vậy thì hiểu nó thế nào?
- Mỗi Input sẽ có một tập giá trị nhất định hoặc một tập các tổ hợp giá trị nhất định, ví dụ chúng ta sẽ có 2 input là
usernamevàpassword, vậy thì với 2 input này, không thể nào lấy giá trị là cụ thể từng username và password, mà cái chúng ta cần là tổ hợp giá trịCorrect username,Incorrect username,Correct password,Incorrect password. Mặt khác, nếu input liên quan đến giá trị số, thì đúng là chúng ta sẽ cần tách chúng ra như EP và BVA, tức là đang kết hợp 3 technique - Sau khi liệt kê ra toàn bộ các bộ giá trị của từng input, chúng ta cấu thành nên bảng quyết định bằng cách tổ hợp chúng vào với nhau để tạo ra các bộ input (Input Combination)
Chúng ta trước mắt sẽ có 4 tổ hợp : Correct username và Correct password, Correct username và Incorrect password, Incorrect username và Correct password, Incorrect username và Incorrect password
Ví dụ, chúng ta sẽ có 2 input là username và password, chúng ta sẽ có bảng quyết định trước khi điền Action như sau:

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
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ố:
- Nếu như có một input không gây ảnh hưởng đến kết quả, thì có thể loại bỏ nó, thay nó bằng dấu
-, ví dụ, cứ hễ làIncorrect usernamethì sẽ không thể login, vậy thì không cần thiết phải kiểm traCorrect passwordhayIncorrect passwordnữa. - Mặt khác, cần loại bỏ các tổ hợp không logic. Nếu xét từ góc độ technical, một Incorrect username là một username chưa có trong database, vậy thì bất kỳ password nào đều cũng không thể đánh giá là đúng hay sai. Vậy nên chúng ta sẽ không đánh giá password khi mà đã có username không đúng, điều này hợp logic với cách giản hoá bên trên

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:
User can login with correct username and passwordUser can't login with correct username and incorrect passwordUser can't login with incorrect username
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ể
User can login with correct username and password->User can login with username: duyvan, password: 123456User can't login with correct username and incorrect password->User can't login with username: duyvan, password: wrong123456User can't login with incorrect username->User can't login with username: wrongduyvan
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:
- Đầu tiên, chúng ta cần xác định trạng thái gốc và trạng thái cuối của flow
- Sau đó, từ trạng thái gốc, chúng ta sẽ xác định tất cả các trạng thái kế tiếp có thể xảy ra cùng các hành động dẫn đến thay đổi trạng thái
- Vẽ State Transition Diagram, một biểu đồ với các trạng thái và các hành động dẫn đến thay đổi trạng thái
- Từ các trạng thái đã xác định, lặp lại như bước 2 và tiếp tục hoàn thiện Diagram
- Cho tới khi tất cả trạng thái trỏ về trạng thái cuối, kết thúc Diagram
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
- Tài khoản mới tạo sẽ ở trạng thái
Active - Tài khoản bị vô hiệu hoá sẽ ở trạng thái
Inactive - Tài khoản bị xoá hoàn toàn sẽ ở trạng thái
Deleted - Tài khoản từ trạng thái
Activecó thể chuyển sangInactivenếu:- Người dùng vào cài đặt tài khoản để vô hiệu hoá
- 6 tháng không có giao dịch
- Vi phạm điều khoản sử dụng mức độ thấp
- Tài khoản từ trạng thái
Activecó thể chuyển sangDeletednếu:- Vi phạm điều khoản sử dụng mức độ cao
- Tài khoản từ trạng thái
Inactivecó thể chuyển sangDeletednếu:- 6 tháng không hồi phục
Dựa vào đây, chúng ta sẽ có 5 test case cho 5 trạng thái chuyển đổi:
User can deactivate account from Active to InactiveAccount automatically change from Active to Inactive after 6 months no transactionAccount automatically change from Active to Inactive after violate low termAccount automatically change from Active to Deleted after violate high termAccount automatically change from Inactive to Deleted after 6 months no recovery
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 > 0 và a <= 0 là đủ.
Vậy thì với Statement Testing sẽ có 2 High Level Test Case:
a is positive with a = 5a is negative with a = -5
Với giá trị -5 và 5 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:
a is positive with a = 5a is not empty with a = 'test'
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:
a is positive with a = 5Nothing happen with a = -1a is not empty with a = 'test'Nothing happen with a = ''
Do rằng, với a = -1 và a = '' 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:
a is positive and b is 1 with a = 5
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.
Nothing happen with a = -1a is positive and b is 1 with a = 5
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:
A is red with A = 'red' and B is red with B = 'red'
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 A và B 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:
A is red with A = 'red' and B is red with B = 'red'Nothing happen with A = 'blue' and B = 'black'
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 A và B, vậy thì chúng ta cần 4 test case để cover toàn bộ Path của function foo:
A is red with A = 'red' and B is red with B = 'red'A is red with A = 'red' and Nothing happen with B = 'black'Nothing happen with A = 'blue' and B is red with B = 'red'Nothing happen with A = 'blue' and B = 'black'
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 code và tất cả nhánh của code chạy ít nhất 1 lần