Data-Flow Analysis Là Gì?
Chủ đề tiếp theo của chúng ta là Phân tích luồng dữ liệu – Data-flow analysis. Data-Flow Analysis là một phương pháp phân tích source code để xác định cách dữ liệu chảy qua các phần của chương trình. Nó tập trung vào việc theo dõi và phân tích cách dữ liệu được tạo ra, truyền đi và sử dụng trong source code.
Điều này bao gồm nhiều kỹ thuật khác nhau để thu thập thông tin về tập giá trị có thể có mà dữ liệu có thể nhận trong quá trình thực thi hệ thống. Trong khi Control-flow analysis liên quan đến các đường dẫn mà luồng thực thi có thể đi qua module code, thì data-flow lại liên quan đến vòng đời của chính dữ liệu đó.
Nếu chúng ta cho rằng một chương trình được thiết kế để tạo, thiết lập, đọc, đánh giá và đưa ra quyết định cũng như hủy dữ liệu thì cần phải xem xét các lỗi có thể xảy ra trong các quá trình này.
Một Số Lỗi Về Luồng Dữ Liệu
Nhiều vấn đề về luồng dữ liệu có liên quan đến ngôn ngữ lập trình đang được sử dụng. Vì một số ngôn ngữ cho phép lập trình viên khai báo ngầm các biến chỉ bằng cách đặt tên và sử dụng chúng. Do đó, một lỗi chính tả có thể gây ra một lỗi nhỏ trong code. Các ngôn ngữ khác sử dụng kiểu dữ liệu rất mạnh trong đó mỗi biến phải được khai báo rõ ràng trước khi sử dụng, nhưng sau đó cho phép lập trình viên “chuyển” biến đó sang một kiểu dữ liệu khác. Giả sử rằng lập trình viên biết mình đang làm gì (đôi khi là một giả định khó hiểu đối với những nhà kiểm thử). Các ngôn ngữ khác nhau có các quy tắc phạm vi dữ liệu có thể tương tác theo những cách tinh tế khác nhau.
Dưới đây là một số ví dụ về lỗi luồng dữ liệu:
- Gán giá trị sai hoặc không hợp lệ cho một biến. Những loại lỗi này bao gồm các vấn đề về chuyển đổi kiểu dữ liệu trong đó trình biên dịch cho phép chuyển đổi nhưng có những tác dụng phụ không mong muốn.
- Dữ liệu nhập sai sẽ dẫn đến việc gán các giá trị không hợp lệ.
- Không xác định được một biến trước khi sử dụng giá trị của nó ở nơi khác.
- Đường dẫn được chọn không chính xác do giá trị không chính xác hoặc không mong đợi được sử dụng trong biến vị ngữ điều khiển.
- Cố gắng sử dụng một biến sau khi nó bị hủy hoặc nằm ngoài phạm vi.
- Định nghĩa lại một biến trước khi nó được sử dụng.
- Tác dụng phụ của việc thay đổi giá trị khi chưa hiểu đầy đủ về phạm vi. Ví dụ: sự thay đổi của biến toàn cục hoặc biến tĩnh có thể gây ra ảnh hưởng đến các quy trình hoặc module khác trong hệ thống.
Set-Use Pairs
Set-Use Pairs (cặp tập hợp-sử dụng) là một khái niệm trong việc phân tích dữ liệu và phân tích luồng dữ liệu. Nó được sử dụng để theo dõi việc sử dụng biến trong chương trình. Ký hiệu luồng dữ liệu có nhiều loại khác nhau; chúng ta sẽ xem xét một trong số chúng ở đây.
- d (define): Đây là thời điểm biến được tạo, xác định hoặc khởi tạo.
- u (use): Biến có thể được sử dụng trong tính toán hoặc trong vị từ quyết định.
- k (kill): Đây là viết tắt của bị hủy, bị tiêu diệt hoặc đã trở nên ngoài phạm vi.
Từ ba hành động nguyên tử này sau đó được kết hợp để hiển thị một luồng dữ liệu. Dấu ~ (dấu ngã) thường được dùng để thể hiện hành động đầu tiên hoặc cuối cùng có thể xảy ra. Dưới đây là 15 tổ hợp mà chúng ta cần quan tâm:
Định nghĩa | Giải thích | ||
---|---|---|---|
1 | ~d | first define | Cho phép. |
2 | du | define-use | Cho phép, trường hợp thông thường. |
3 | dk | define-kill | Bug tiềm ẩn; dữ liệu chưa từng được sử dụng. |
4 | ~u | first use | Bug tiềm ẩn; dữ liệu được sử dụng mà không định nghĩa. Nó có thể là biến toàn cục, định nghĩa bên ngoài. |
5 | ud | use-define | Cho phép; dữ liệu được sử dụng sau đó định nghĩa lại. |
6 | uk | use-kill | Cho phép. |
7 | ~k | first kill | Bug tiềm ẩn; dữ liệu bị hủy trước khi định nghĩa. |
8 | ku | kill-use | Bug nghiêm trọng dữ liệu được sử dụng sau khi bị hủy. |
9 | kd | kill-define | Luôn cho phép. Dữ liệu bị hủy và sau đó định nghĩa lại. Một số giả thuyết cho rằng điều này không được phép. |
10 | dd | define-define | Bug tiềm ẩn; định nghĩa hai lần. |
11 | uu | use-use | Cho phép; trường hợp thông thường. Đôi khi không cần bận tâm đến việc kiểm tra cặp này vì không xảy ra việc định nghĩa lại. |
12 | kk | kill-kill | Có thể là lỗi. |
13 | d~ | define last | Bug tiềm ẩn; đây có phải biến chết không? Nó có thể là biến toàn cục được sử dụng ở ngữ cảnh khác. |
14 | u~ | use last | Cho phép. Biến được sử dụng ở chu trình này nhưng không bị hủy. |
15 | k~ | kill last | Cho phép; trường hợp thông thường |
Ví Dụ Về Set-Use Pair
Xem xét tình huống sau đây:
Giả sử rằng một công ty điện thoại cung cấp gói điện thoại di động sau: Nếu khách hàng sử dụng tối đa 100 phút, thì gói cước sẽ phải trả một khoản phí cố định là 40 USD. Đối với tất cả số phút sử dụng từ 101 đến 200 phút, sẽ có khoản phí bổ sung là 0,5 cent mỗi phút. Bất kỳ phút nào được sử dụng sau đó sẽ được tính phí 0,10 USD mỗi phút. Cuối cùng, nếu hóa đơn trên 100 đô la trở lên, sẽ được giảm giá 10% cho toàn bộ hóa đơn.
Xem xét tiếp đoạn code xử lý cho tình huống trên như sau:
public static double calculateBill (int Usage) {
double Bill = 0;
if (Usage > 0) {
Bill = 40;
if (Usage > 100) {
if (Usage <= 200) {
Bill = Bill + (Usage - 100) * 0.5;
}
else {
Bill = Bill + 50 + (Usage - 200) * 0.1;
if (Bill >= 100) {
Bill = Bill * 0.9;
}
}
}
return Bill;
}
Đối với mỗi iến, chúng ta có thể tạo danh sách mẫu d-u-k để theo dõi tất cả các thay đổi đối với biến đó thông qua module.
Xem xét luồng xử lý dữ liệu cho biến Usage, ta có bảng như sau:
No. | Pattern | Explanation |
---|---|---|
1 | ~d (1) | Case thông thường |
2 | du (1-3) | Case thông thường |
3 | uu (3-6)(6-7)(7-8)(7-11) | Case thông thường |
4 | uk (6-19)(8-19)(11-19) | Case thông thường |
5 | k~ (19) | Case thông thường |
Hình: Ví dụ về Control-flow diagram cho biến Usage
- Biến Usage được tạo ở dòng 1. Nó thực sự là một tham số hình thức được truyền vào dưới dạng đối số khi hàm được gọi. Trong hầu hết các ngôn ngữ, đây sẽ là một biến được tạo trên ngăn xếp và được đặt ngay lập tức thành giá trị được truyền vào.
- Đây là cặp du (define-use). Điều này chỉ đơn giản nói rằng biến được định nghĩa ở dòng 1 và được sử dụng ở dòng 3. Đây là hành vi được mong đợi.
- Mỗi lần Usage được sử dụng theo cặp du trước đó. Nó sẽ là cặp uu (use-use) cho đến khi được định nghĩa lại hoặc bị hủy.
- Biến Usage được sử dụng ở dòng (3-6), (6-7), (7-8) và (7-11). Lưu ý rằng không có cặp (8-11) vì trong mọi trường hợp, chúng ta không thể thực hiện đường dẫn đó. Dòng 7 nằm trong nhánh TRUE của câu điều kiện và dòng 11 nằm trong đường dẫn FALSE.
- Theo uk (use-kill), có ba cặp có thể xảy ra mà chúng ta phải giải quyết.
- (6-19) có thể thực hiện được khi Usage có giá trị từ 100 trở xuống. Chúng ta sử dụng nó ở dòng 6 và lần chạm tiếp theo là khi hàm kết thúc. Vào thời điểm đó, khung ngăn xếp không được kiểm soát và biến biến mất.
- (8-19) có thể sử dụng được khi Usage sử dụng nằm trong khoảng từ 101 đến 200. Giá trị của Bill được đặt và sau đó chúng ta quay lại.
- (11-19) có thể thực hiện được khi Usage lớn hơn 200.
- Lưu ý rằng (3-19) và (7-19) đều không được vì trong mỗi trường hợp chúng ta phải chạm lại vào Usage trước khi nó bị hủy. Đối với dòng 3, chúng ta phải sử dụng lại ở dòng 6. Đối với dòng 7, chúng ta phải sử dụng lại ở dòng 8 hoặc 11.
- Cuối cùng, ở dòng 19, chúng ta có hủy cuối cùng ở Usage vì khung ngăn xếp đã bị loại bỏ.
Ngoài xem xét luồng xử lý dữ liệu cho biến Usage, chúng ta cũng có thể xem xét cho biến local Bill. Cùng mình tiếp tục phân tích luồng xử lý dữ liệu cho biến local Bill trong bảng sau đây nhé.
No. | Pattern | Explanation |
---|---|---|
1 | ~d (2) | Case thông thường |
2 | dd (2-4) | Case nghi ngờ |
3 | du (2-18)(4-8)(4-11)(11-12) | Case thông thường |
4 | ud (8-8)(11-11)(13-13) | Case chấp nhận được |
5 | uu (12-13)(12-18) | Case thông thường |
6 | uk (18-19) | Case thông thường |
7 | k~ (19) | Case thông thường |
Hình: Ví dụ về Control-flow diagram cho biến Bill
- ~d biểu thị rằng đây là lần đầu tiên biến này được định nghĩa. Điều này là bình thường đối với việc khai báo biến cục bộ.
- dd (define-define) nên được coi là đáng ngờ. Nói chung, chúng ta không muốn thấy một biến được xác định lại trước khi nó được sử dụng. Tuy nhiên, trong trường hợp này thì ổn; cần phải đảm bảo rằng Bill được xác định trước lần sử dụng đầu tiên mặc dù nó có thể được xác định lại. Lưu ý rằng nếu chúng ta không đặt giá trị ở dòng 2 thì nếu không có số phút điện thoại nào cả, chúng ta sẽ trả về một giá trị không xác định ở cuối. Nó có thể bằng 0, hoặc có thể không. Trong một số ngôn ngữ, không được phép gán giá trị trong câu lệnh có khai báo biến. Trong trường hợp như vậy, câu lệnh if() trên dòng 3 có thể sẽ được cung cấp một mệnh đề else trong đó Bill sẽ được đặt thành giá trị 0. Cách viết code này hiện có thể hiệu quả hơn so với việc có một bước nhảy khác cho mệnh đề else .
- du (define-use) có nhiều cách dùng khác nhau:
- Cặp (2-18) xảy ra khi không có số phút gọi điện và đảm bảo rằng chúng không trả về giá trị không xác định.
- Cặp (4-8) xảy ra khi Usage nằm trong khoảng từ 101 đến 200. Xin lưu ý rằng phần sử dụng của nó nằm ở phía bên phải của câu lệnh không phải bên trái. Giá trị trong Bill phải được truy xuất để thực hiện tính toán và sau đó nó được truy cập lại (sau khi tính toán) để lưu trữ.
- Cặp (4-11) xảy ra khi Usage trên 200 phút. Một lần nữa, nó được sử dụng ở phía bên phải của câu lệnh, không phải bên trái.
- Cặp (11-12) xảy ra khi chúng ta đặt lại giá trị (ở phía bên trái của câu lệnh ở dòng 11) rồi quay lại và sử dụng nó trong câu điều kiện ở dòng 12.
- ud (use-define) xảy ra khi chúng ta gán một giá trị mới cho một biến. Lưu ý rằng ở dòng 8, 11 và 13, chúng ta gán giá trị mới cho biến Bill. Trong mỗi trường hợp, chúng cũng được sử dụng giá trị cũ cho Bill để tính giá trị mới. Lưu ý rằng ở dòng 4, chúng ta không sử dụng giá trị Bill ở phía bên phải của câu lệnh. Tuy nhiên, vì chúng ta không thực sự sử dụng Bill trước dòng đó nên nó không phải là cặp ud; thay vào đó, như đã đề cập ở (2), nó là một cặp dd. Trong mỗi trường hợp, đây được coi là hành vi có thể chấp nhận được.
- uu (use-use) xảy ra ở dòng (12-13). Trong trường hợp này, giá trị của Bill được sử dụng trong biểu thức quyết định ở dòng 12 và sau đó được sử dụng lại ở phía bên phải của câu lệnh ở dòng 13. Nó cũng có thể xảy ra ở (12-18) nếu giá trị của Bill nhỏ hơn $100 .
- uk (use-kill) chỉ có thể xảy ra một lần trong đoạn code này (18-19) vì tất cả việc thực thi được tuần tự qua dòng 18.
- k~ (kill last) xảy ra khi hàm kết thúc ở dòng 19. Vì biến cục bộ tự động vượt ra khỏi phạm vi khi hàm trả về nên nó sẽ bị hủy.
Trên đây là phân tích luồng dữ liệu cho biến Usage và Bill. Tuy nhiên, nếu bạn để ý sẽ có thể đặt câu hỏi “Sẽ phải trả bao nhiêu nếu người dùng hoàn toàn không sử dụng điện thoại di động. Giả định của chúng ta là họ vẫn phải trả 40 USD. Tuy nhiên, code xử lý bên trên, người dùng sẽ không cần trả phí.” Dòng 3 xem xét số phút được tính phí. Nếu không có hóa đơn nào thì số tiền 40 USD ban đầu sẽ không được lập hóa đơn. Thay vào đó, nó đánh giá là FALSE và một hóa đơn 0,00 USD được gửi. Đây có thể xem xét như một bug.
Kết Luận
Thực tế là không phải tất cả các dữ liệu bất thường đều là khiếm khuyết. Những lập trình viên thông minh thường làm những điều kỳ lạ và đôi khi thậm chí còn có những lý do chính đáng để làm điều đó. Được viết theo một cách nhất định, code có thể thực thi nhanh hơn. Một nhà phân tích kiểm tra kỹ thuật giỏi phải có khả năng điều tra cách sử dụng dữ liệu, bất kể lập trình viên giỏi đến đâu; ngay cả những lập trình viên giỏi cũng tạo ra lỗi.
Thật không may, như chúng ta sẽ thấy, phân tích luồng dữ liệu không phải là phương pháp khắc phục phổ biến cho tất cả các cách có thể xảy ra lỗi. Đôi khi code tĩnh sẽ không chứa đủ thông tin để xác định liệu có lỗi hay không. Ví dụ: dữ liệu tĩnh có thể chỉ đơn giản là một con trỏ vào một cấu trúc động không tồn tại cho đến khi chạy. Chúng ta có thể không biết khi nào một quy trình hoặc luồng khác sẽ thay đổi biến điều kiện, sẽ cực kỳ khó theo dõi ngay cả khi thử nghiệm động.
Điều quan trọng cần nhớ rằng thử nghiệm là một quá trình sàng lọc. Chúng ta có thể tìm thấy một số lỗi với kỹ thuật này, một số với kỹ thuật khác. Nhiều khi việc phân tích luồng dữ liệu sẽ tìm ra những khiếm khuyết có thể xảy ra. Như mọi khi, hãy sử dụng các kỹ thuật mà chúng ta có thể chi trả được, dựa trên bối cảnh của dự án. Hãy coi phân tích luồng dữ liệu là một vũ khí nữa trong kho vũ khí thử nghiệm của bạn.
Bài viết hôm nay khá dài. Mình xin kết thúc bài chia sẻ hôm nay tại đây. Hẹn gặp lại các bạn trong các bài viết tiếp theo.
Happy Testing!