Bài giảng Kỹ thuật lập trình - Chương 4: Các kỹ thuật kiểm tra tính đúng đắn và tính an toàn của chương trình phần mềm - Vũ Thị Hương Giang

Lỗi: chương trình chạy không đúng như đã định Chuỗi kiểm tra chương trình

Bẫy lỗi (error handling)

Lập trình phòng ngừa (defensive programming)

Kiểm thử (testing)

– Gỡ rối (debugging)

Phân biệt:

Bẫy lỗi: Prevent errors

Lập trình phòng ngừa: Detect problems as early as possible

Kiểm thử: finished code

- Gỡ rối: fixing defects uncovered by testing

pdf 128 trang xuanthi 27/12/2022 2560
Bạn đang xem 20 trang mẫu của tài liệu "Bài giảng Kỹ thuật lập trình - Chương 4: Các kỹ thuật kiểm tra tính đúng đắn và tính an toàn của chương trình phần mềm - Vũ Thị Hương Giang", để tải tài liệu gốc về máy hãy click vào nút Download ở trên.

File đính kèm:

  • pdfbai_giang_ky_thuat_lap_trinh_chuong_4_cac_ky_thuat_kiem_tra.pdf

Nội dung text: Bài giảng Kỹ thuật lập trình - Chương 4: Các kỹ thuật kiểm tra tính đúng đắn và tính an toàn của chương trình phần mềm - Vũ Thị Hương Giang

  1. Tràn số (overflows of numbers) • Nếu cần tính toán với các số lớn, hãy chắc chắn là bạn biết giá trị lớn nhất mà biến bạn dùng có khả năng lưu trữ • Ví dụ: – Với phần lớn trình dịch C, 1 unsigned char có giá trị từ 0 đến 255. – Kích thước tối đa của 1 biến kiểu int có thể thay đổi
  2. Chắc chắn hay chính xác ? • Chắc chắn: CT luôn chạy thông, kể cả khi có lỗi • Chính xác: CT không bao giờ gặp lại lỗi • Ví dụ: Lỗi hiện thị trong các trình xử lý văn bản: khi đang thay đổi nội dung văn bản, thỉnh thoảng một phần của một dòng văn bản ở phía dưới màn hình bị hiện thị sai. Khi đó người dùng phải làm gì? – Tắt CT – Nhấn PgUp hoặc PgDn, màn hình sẽ làm mới • Ưu tiên tính chắc chắn thay vì tính chính xác: – Bất cứ kết quả nào đó bao giờ cũng thường là tốt hơn so với Shutdown.
  3. 5. Xử lý ngoại lệ • Một chương trình biên dịch thành công, vẫn có thể gây ra những lỗi khi chạy, đó chính là các ngoại lệ (exception). – Điều kiện chủ quan: Do lập trình sai – Điều kiện khách quan: Do nhập sai dữ liệu, do trạng thái của hệ thống (tràn bộ nhớ). • Ngoại lệ phá vỡ luồng bình thường của CT • Khi xảy ra một ngoại lệ, nếu không xử lý thì chương trình kết thúc ngay và trả lại quyền điều khiển cho hệ điều hành. float number1, number2; NO HANDLER EXISTS //nhập vào 2 số float division=number1/number2; EXIT
  4. Báo hiệu điều kiện lỗi • Thông báo cho các bộ phận khác của chương trình về lỗi không nên bỏ qua • Chỉ dùng ngoại lệ cho những điều kiện thực sự ngoại lệ – Exception được dùng trong những tình huống giống assertion cho các sự kiện không thường xuyên, nhưng có thể không bao giờ xảy ra – Exception có thể bị lạm dụng và phá vỡ các cấu trúc, điều này dễ gây ra lỗi, vì làm sai lệch luồng điều khiển
  5. Phục hồi tài nguyên khi có ngoại lệ • Thường thì không phục hồi tài nguyên • Nhưng sẽ hữu ích khi thực hiện các công việc nhằm đảm bảo cho thông tin ở trạng thái rõ ràng và vô hại nhất có thể • Nếu các biến vẫn còn được truy xuất thì chúng nên được gán các giá trị hợp lý • Trường hợp thực thi việc cập nhật dữ liệu, nhất là trong 1 phiên – transaction – liên quan tới nhiều bảng chính, phụ, thì việc khôi phục khi có ngoại lệ là vô cùng cần thiết (rollback)
  6. Kiểm tra mã nguồn trong khi viết chương trình • 1 LTV giỏi không ngồi viết 1 lúc 10000 dòng lệnh rồi chạy thử chương trình • Mọi việc sẽ dễ dàng hơn khi vừa viết chương trình vừa kiểm tra • Xác định cấu trúc của chương trình, viết từng phần nhỏ của chương trình và kiểm tra luôn phần vừa viết. • Hoàn thiện từng phần chương trình, kiểm tra lại sau khi hoàn thiện. • Mẹo: – Nếu có sửa đổi mã nguồn thì dịch từng đoạn một, khoảng chục dòng – Sử dụng nhiều cửa sổ dịch
  7. a. Mục đích • Khó có thể khẳng định 1 CT lớn có làm việc chuẩn hay không • Khi XD 1 CT lớn, 1 LTV chuyên nghiệp sẽ dành thời gian cho việc viết test code không ít hơn thời gian dành cho viết CT • LTV chuyên nghiệp là người có khả năng, kiến thức rộng về các kỹ thuật và chiến lược testing
  8. c. Testing vs. debugging • Testing & debugging đi cùng với nhau như 1 cặp: – Testing tìm errors; debugging định vị và sửa chúng. – Ta có mô hình “testing/debugging cycle”: Ta test, rồi debug, rồi lặp lại. – Bất kỳ 1 debugging nào nên được tiếp theo là 1 sự áp dụng lại của hàng loạt các tests liên quan, đặc biệt là các bài tests hồi quy. Điều này giúp tránh nảy sinh các lỗi mới khi debugging. – Testing & debugging không nên được thực hiện bởi cùng 1 người.
  9. e. Nguyên tắc • Ưu tiên việc kiểm thử, để khi kết thúc quá trình kiểm thử, luôn bảo đảm là việc kiểm thử đã được thực hiện tốt nhất có thể trong khoảng thời gian bạn có.
  10. Khái niệm • External testing: Thiết kế dữ liệu để kiểm thử chương trình • Phân loại : – Kiểm thử giá trị biên : Boundary testing – Kiểm thử lệnh : Statement testing – Kiểm thử luồng : Path testing – Kiểm thử sốc : Stress testing
  11. Ví dụ • VD : đọc 1 dòng từ stdin và đưa vào mảng ký tự int i; char s[MAXLINE]; for (i=0; (s[i]=getchar()) != '\n' && i in ra “||” , ok – Nếu gặp EOF trước '\n‘ • Tiếp tục gọi getchar() và lưu ӱ vào s[i], not ok – Nếu gặp ngay EOF (empty file) • Tiếp tục gọi getchar() và lưu ӱ vào s[i], not ok
  12. Boundary Testing Example (cont.) int i; char s[MAXLINE]; • Rewrite the code for (i=0; i<MAXLINE-1; i++) if ((s[i] = getchar()) == '\n') break; s[i] = '\0'; • Trường hợp gặp EOF for (i=0; i<MAXLINE-1; i++) if ((s[i] = getchar()) == '\n' || s[i] == EOF) break; s[i] = '\0'; • Các trường hợp khác? – Nearly full – Exactly full This is wrong. Why? – Over full
  13. Ambiguity in Specification • Nếu dòng quá dài, xử lý thế nào? – Giữ MAXLINE ký tự đầu, bỏ qua phần còn lại? – Giữ MAXLINE ký tự đầu + ‘\0’, bỏ qua phần còn lại? – Giữ MAXLINE ký tự đầu+’\0’, lưu phần còn lại cho lần gọi sau của input function? • Có thể phần đặc tả - specification không hề đề cập khi MAXLINE bị quá – Có thể người ta không muốn dòng dài quá giới hạn trong mọi trường hợp – Đạo đức: kiểm tra đã phát hiện ra một vấn đề thiết kế, thậm chí có thể là một vấn đề đặc điểm kỹ thuật ! • Quyết định phải làm gì – Cắt những dòng quá dài? – Lưu phần còn lại để đọc như 1 dòng mới?
  14. b. Statement Testing • “Testing để thỏa mãn điều kiện rằng mỗi lệnh trong 1 CT phải thực hiện ít nhất 1 lần khi testing.”
  15. c. Path Testing • Kiểm tra để đáp ứng các tiêu chuẩn đảm bảo rằng mỗi đường dẫn logic xuyên suốt chương trình được kiểm tra. Thường thì đường dẫn xuyên suốt chương trình này được nhóm thành một tập hữu hạn các lớp. Một đường dẫn từ mỗi lớp sau đó được kiểm tra. • Khó hơn nhiều so với statement testing – Với các CT đơn giản, có thể liệt kê các nhánh đường dẫn xuyên suốt code – Ngược lại, bằng các đầu vào ngẫu nhiên tạo các đường dẫn theo CT
  16. Bài tập (1) input(A,B) if (A>0) 1 (2) Z = A; else A>0 (3) Z = 0; F T if (B>0) 3 2 (4) Z = Z+B; B>0 T (5) output(Z) F 4 5 What is the path condition for path ? (A>0) Л (B 0)
  17. Consider ANOTHER example (1) input(A,B) if (A>B) 1 (2) B = B*B; A>B T if (B ? (A>B) Л (B<0) (B2<0) = FALSE
  18. Ví dụ: Stress Testing • Example program: #include int main(void) { Stress testing: Phải cung cấp char c; các đầu vào ngầu nhiên dạng while ((c = getchar()) != EOF) nhị phân hay ASCII putchar(c); return 0; } • Mục tiêu: Copy tất cả các ký tự từ stdin vào stdout; nhưng lưu ý bug!!! • Làm việc với tập dữ liệu ASCII chuẩn ( tự tạo) • Máy tính tự tạo ngẫu nhiên tập dữ liệu dạng 255 (decimal), hay 11111111 (binary), và EOF để dừng vòng lặp
  19. 3. Internal Testing • Internal testing: Thiết kế chương trình để nó tự kiểm thử chính nó • Internal testing techniques (1) Kiểm tra các giá trị bất biến - Testing invariants (2) Kiểm tra các thuộc tính cần được bảo lưu - Verifying conservation properties (3) Kiểm tra các giá trị trả về - Checking function return values (4) Thay đổi mã nguồn tạm thời - Changing code temporarily (5) Giữ lại phần mã kiểm tra - Leaving testing code intact
  20. b. Kiểm tra các đặc tính cần được bảo lưu • Khái quát hóa của testing invariants • 1 hàm cần kiểm tra các cấu trúc dữ liệu bị tác động tại các điểm đầu và cuối • VD: hàm Str_concat() – Tại điểm đầu, tìm độ dài của 2 xâu đã cho; tính tổng – Tại điểm cuối, tìm độ dài của xâu kết quả – 2 độ dài có bằng nhau không ? • VD: Hàm chèn thêm PT vào danh sách -List insertion function – Tại điểm khởi đầu, tính độ dài ds – Tại điểm cuối, Tính độ dài mới – Độ dài mới = độ dài cũ + 1?
  21. Kiểm tra các giá trị trả về – scanf() trả về số giá trị đã đọc Bad code Good code int i; int i; scanf("%d", &i); if (scanf("%d", &i) != 1) /* Error */ – printf() có thể bị lỗi nếu ghi vào file hay đĩa đầy; trả về số các ký tự (ko phải giá trị) đã ghi Bad code??? Good code, or overkill??? int i = 100; int i = 100; printf("%d", i); if (printf("%d", i) != 3) /* Error */
  22. Thay đổi mã nguồn tạm thời • Viết 1 phiên bản hàm cấp phát bộ nhớ và phát hiện ra lỗi sớm, để kiểm thử đoạn mã nguồn bị lỗi thiếu bộ nhớ: void *testmalloc( size_t n) { static int count =0; if (++count > 10) return NULL; else return malloc(n); }
  23. 4. Các chiến lược kiểm thử tổng quát • Kiểm thử dần dần -Testing incrementally • So sánh các cách cài đặt khác nhau -Comparing implementations • Tự động kiểm thử – Automation • Kiểm thử theo lỗi - Bug-driven testing • Tiêm, gài lỗi - Fault injection
  24. Testing Incrementally (cont.) • Tạo “giàn giáo” - scaffolds và “mẫu” -stubs để test đoạn code mà ta quan tâm Hàm gọi đến code mà ta quan tâm Đoạn code cần quan tâm Hàm được gọi Hàm được gọi bởi đoạn code cần bởi đoạn code cần quan tâm quan tâm
  25. c. Tự động kiểm thử • Kiểm thử thủ công rất nặng nhọc, tốn kém và nhàm chán thậm chí không đủ độ tin cậy. • Trong quá trình kiểm thử, phải – Thực hiện lặp đi lặp lại các thao tác kiểm thử – Dùng nhiều bộ dữ liệu nhập – Nhiều lần so sánh dữ liệu đầu ra • Tạo testing code để tránh các nhược điểm nói trên – Viết 1 bộ kiểm thử để kiểm tra toàn bộ chương trình mỗi khi có sự thay đổi, sau khi biên dịch thành công – Cần biết cái gì được chờ đợi • Tạo ra các đầu ra, sao cho dễ dàng nhận biết là đúng hay sai – Tự động kiểm thử tốt hơn nhiều so với kiểm thử thủ công
  26. Tạo ra những kiểm thử độc lập • Kiểm thử độc lập với các giá trị nhập và giá trị xuất mong đợi sẽ bổ xung cho kiểm thử hồi quy • VD: Dùng ngôn ngữ NewAwk thực hiện kiểm thử 1 CT ngắn, ghi dữ liệu đẩu ra của CT vào 1 tệp, ghi kết quả đúng vào 1 tệp khác rồi so sánh 2 tệp, nếu khác nhau thì thông báo lỗi echo 3 5 | newawk ‘{i=1; print ($si)++; print $i ,i}’ > out1 echo ‘3 4 1’ > out2 #kết quả đúng if ! Cmp –s out1 out2 # nếu kq so sánh khác nhau then echo ‘BAD: test failed’ Fi • Mọt lỗi có thể cần nhiều thao tác kiểm thử hoặc phải kiểm tra toàn bộ các lớp mới, hoặc có thể thêm vào những đoạn CT bảo vệ để có thể bắt đc những lỗi trong CT
  27. e. Fault injection • Chủ động (tạm thời) cài các bugs!!! • Rồi quyết định nếu tìm thấy chúng • Kiểm thử chính đoạn mã kiểm thử/ quy trình kiểm thử
  28. Các kỹ thuật kiểm thử • Black-Box: Testing chỉ dựa trên việc phân tích các yêu cầu - requirements (unit/component specification, user documentation, v.v.). Còn được gọi là functional testing. • White-Box: Testing dựa trên việc phân tích các logic bên trong - internal logic (design, code, v.v.). (Nhưng kết quả mong đợi vẫn đến từ requirements.) Còn đc gọi là structural testing.
  29. Tại sao không "test everything"? Chi phí cho 'exhaustive' testing: 20 x 4 x 3 x 10 x 2 x 100 = 480,000 tests Nếu 1 giây cho 1 test, 8000 phút, 133 giờ, 17.7 ngày (chưa kể nhầm lẫn hoặc test đi test lại) nếu 10 secs = 34 wks, 1 min = 4 yrs, 10 min = 40 yrs
  30. IV. GỠ RỐI 1. Tổng quan về gỡ rối chương trình 2. Tìm kiếm và gỡ rối
  31. 1. Tổng quan về gỡ rối chương trình • Gỡ rối là gì ? – Khi chương trình bị lỗi, gỡ rối là các công việc cần làm để làm cho chương trình dịch thông, chạy thông – Thật không may, gỡ rối luôn là thao tác phải làm khi lập trình, thao tác này rất tốn kém • Cách tốt nhất vẫn là phòng ngừa – Khi bắt đầu gỡ rối chương trình, bạn đã biết là chương trình không chạy. – Nếu bạn biết lý do tại sao chương trình không chạy, bạn có thể sửa được chương trình cho nó chạy – Nếu bạn hiểu chương trình của bạn, bạn sẽ có ít sai lầm và dễ dàng sửa chữa sai sót hơn. Bí quyết là viết mã đơn giản, hiệu quả, chú thích hợp lý. • Đối với mã nguồn, tiêu chí nào quan trọng hơn: rõ ràng hay chính xác ? – Nếu mã nguồn rõ ràng, bạn có thể làm cho chương trình trở nên chính xác. – Bạn có chắc là làm cho chương trình trở nên chính xác nếu nó không rõ ràng hay không ? • Nếu chương trình được thiết kế với cấu trúc tốt, được viết bằng phong cách lập trình tốt và áp dụng các kỹ thuật viết chương trình hiệu quả, bẫy lỗi thì chi phí cho việc gỡ rối sẽ được giảm thiểu.
  32. 2. Tìm kiếm và gỡ rối Debugging Heuristic Áp dụng khi nào (1) Hiểu các thông báo lỗi (error messages) Build-time (dịch) (2) Nghĩ trước khi viết lại chương trình (3) Tìm kiếm các lỗi (bug) hay xảy ra (4) Divide and conquer Run-time (chạy) (5) Viết thêm các đoạn mã kiểm tra để chương trình tự kiểm tra nó (6) Hiện thị kết quả (7) Sử dụng debugger (8) Tập trung vào các lệnh mới viết / mới viết lại
  33. Hiểu các thông báo lỗi • Một số thông báo lỗi đến từ compiler #include int main(void) /* Print "hello, world" to stdout and return 0. */ { printf("hello, world\n") retun 0; }
  34. 2.2. Nghĩ trước khi viết lại chương trình Việc thay đổi mã nguồn không hợp lý có thể gây ra nhiều vấn đề hơn là để nguyên không thay đổi gì, nên Phải nghĩ trước khi làm
  35. Nghĩ trước khi viết lại chương trình • Tạm dừng viết CT – Khi gặp vấn đề, khó khăn, chậm tiến độ, lập tức thay đổi công việc => rút ra khỏi luồng quán tính sai lầm – Bỏ qua đoạn CT có lỗi – Khi nào cảm thấy sẵn sàng thì chữa • Giải thích logic của đoạn mã nguồn: – Cho chính bạn • Tạo điều kiện để suy nghĩ lại – Cho ai khác có thể phản bác • Extrem programming : làm việc theo cặp, pair programming, người này LT, người kia kiểm tra, và ngược lại – Cho cái gì đó không thể phản bác (cây, cốc trà đá, gấu bông, cún, ) • Tạo điều kiện củng cố suy luận của mình
  36. Tìm kiếm các lỗi hay xảy ra • Khi gặp vấn đề, hãy liên tưởng đến những trường hợp tương tự đã gặp – Vd1 : int n; scanf(“%d”,n); ? – Vd2 : int n=1; double d=PI; printf(“%d %f \n”,d,n); ?? • Không khởi tạo biến (với C) cũng sẽ gây ra những lỗi khó lường.
  37. Các phương pháp gỡ rối • Tránh mắc cùng 1 lỗi 2 lần : Sau khi sửa 1 lỗi, hãy suy nghĩ xem có lỗi tương tự ở nơi nào khác không. VD : for (i=1;i<argc;i++) { if (argv[i][0] !=‘-’) break; switch (argv[i][1]) { case ‘o’ : /* tên tệp ouput*/ outname = argv[i]; break; case ‘f’ : from = atoi(argv[i]); break; case ‘t’ : to = atoi(argv[i]); break; } } Chú ý : nếu có thể code khi ngủ thì cũng đừng ngủ gật khi code
  38. Divide and Conquer • Khử đầu vào – Thử chương trình với các tham số đầu vào từ đơn giản đến phức tạp, từ nhỏ đến lớn để tìm lỗi – Ví dụ: chương trình lỗi với file đầu vào filex • Tạo ra phiên bản copy của filex , tên là filexcopy • Xoá bớt nửa sau của filexcopy • Chạy chương trình với tham số đầu vào là filexcopy – Nếu chạy thông => nửa đầu của filex không gây lỗi, loại bỏ nửa này, tìm lỗi trong nửa sau của filex – Nếu không chạy => nửa đầu của filex gây lỗi, loại bỏ nửa sau, tìm lỗi trong nửa đầu của filex • Lặp cho đến khi không còn dòng nào trong filex có thể bị loại bỏ – Cách khác: bắt đầu với 1 ít dòng trong filex, thêm dần các dòng vào cho đến khi thấy lỗi
  39. 2.5. Viết thêm các đoạn mã kiểm tra để chương trình tự kiểm tra nó • Dùng internal test để khử lỗi trong CT và giảm nhẹ công việc tìm kiếm lỗi – Chỉ cần viết thêm 1 hàm để kiểm tra, gắn vào trước và sau đoạn có nguy cơ, comment lại sau khi đã xử lý xong lỗi – Các kỹ thuật viết thêm mã tự kiểm tra cho chương trình đã học • Kiểm tra các giá trị không thay đổi • Kiểm tra các đặc tính cần bảo lưu • Kiểm tra giá trị trả về của hàm • Thay đổi mã nguồn tạm thời • Kiểm tra mà không làm thay đổi mã nguồn • Dùng assertion để nhận dạng các lỗi có trong CT
  40. Assertions • Assertions có thể được dùng để kiểm tra các giả thiết như : – Các tham số đầu vào nằm trong phạm vi mong đợi (tương tự với các tham số đầu ra) – File hay stream đang được mở (hay đóng) khi 1 CTC bắt đầu thực hiện (hay kết thúc) – 1 file hay stream đang ở bản ghi đầu tiên (hay cuối cùng) khi 1 CTC bắt đầu ( hay kết thúc) thực hiện – 1 file hay stream được mở để đọc, để ghi, hay cả đọc và ghi – Giá trị của 1 tham số đầu vào là không thay đổi bởi 1 CTC – 1 pointer là non-NULL – 1 mảng đc truyền vào CTC có thể chứa ít nhất X phần tử – 1 bảng đã đc khởi tạo để chứa các giá trị thực – 1 danh sách là rỗng (hay đầy) lkhi 1 CTC bắt đầu (hay kết thúc) thực hiện
  41. Dùng assertions như thế nào ? • Bẫy lỗi cho những tình huống lường trước (sự kiện ta chờ đợi sẽ xảy ra); – Error-handling : checks for bad input data Hướng tới việc xử lý lỗi • Dùng assertions cho các tình huống không lường trước (sự kiện không mong đợi xảy ra hoặc không bao giờ xảy ra) – Assertions : check for bugs in the code hướng đến việc hiệu chỉnh chương trình, tạo ra phiên bản mới của chương trình • Tránh đưa code xử lý vào trong assertions – Điều gì xảy ra khi ta turn off the assertions ?
  42. Kiểm tra các giá trị không thay đổi • Có thể sử dụng assert để kiểm tra các giá trị không thay đổi #ifndef NDEBUG int isValid(MyType object) { Test invariants here. Return 1 (TRUE) if object passes all tests, and 0 (FALSE) otherwise. } #endif void myFunction(MyType object) { assert(isValid(object)); Manipulate object here. assert(isValid(object)); }
  43. Hiện thị kết quả đầu ra • Tạo log file • Lưu vết In debugging – Giúp ghi nhớ đc các vấn đề đã xảy ra, output ra stderr; và giải quyết các vđề tương tự sau debugging output này, cũng như khi chuyển giao CT có thể tách biệt cho người khác với đầu ra thông • Maybe even better: thường bằng cách in ấn của CT fprintf(stderr, "%d", keyvariable); Ngoài ra: stderr • Maybe better still: không dùng buffer FILE *fp = fopen("logfile", "w"); fprintf(fp, "%d", keyvariable); Ghi ra 1 a log file fflush(fp);
  44. Sử dụng trình gỡ rối • Dùng 1 trình gỡ rối để chạy từng bước là phương sách cuối cùng • Nhiều khi vấn đề tưởng quá đơn giản nhưng lại không phát hiện được, ví dụ các toán tử so sánh trong pascal va VB có độ ưu tiên ngang nhau, nhưng với C ? ( == và != nhỏ hơn !) • Thứ tự các đối số của lời gọi hàm : ví dụ : strcpy(s1,s2) int m[6]={1,2,3,4,5,6}, *p,*q; p=m; q=p+2; *p++ =*q++; *p=*q; ??? • Lỗi loại này khó tìm vì bản thân ý nghĩ của ta vạch ra 1 hướng suy nghĩ sai lệch : coi điều không đúng là đúng • Đôi khi lỗi là do nguyên nhân khách quan : Trình biên dịch, thư viện hay hệ điều hành, hoặc lỗi phần cứng : 1994 lỗi xử lý dấu chấm độngng trong bộ xử lý Pentium
  45. 2.8. Tập trung vào các lệnh mới viết / mới viết lại • Kiểm tra sự thay đổi mới nhất – Lỗi thường xảy ra ở những đoạn CT mới được bổ sung – Nếu phiên bản cũ OK, phiên bản mới có lỗi => lỗi chắc chắn nằm ở những đoạn CT mới – Lưu ý, khi sửa đổi, nâng cấp : hãy giữ lại phiên bản cũ – đơn giản là comment lại đoạn mã cũ – Đặc biệt, với các hệ thống lớn, làm việc nhóm thì việc sử dụng các hệ thống quản lý phiên bản mã nguồn và các cơ chế lưu lại quá trình sửa đổi là vô cùng hữu ích ( source safe )
  46. Tập trung vào các lệnh mới viết / mới viết lại • Phải gỡ rối ngay, không nên để sau – Khó: Viết toàn bộ chương trình; kiểm tra toàn bộ chương trình, gỡ rối toàn bộ chương trình – Dễ hơn: Viết từng đoạn, kiểm tra từng đoạn, gỡ rối từng đoạn; viết từng đoạn, kiểm tra từng đoạn, gỡ rối từng đoạn; • Nên giữ lại các phiên bản trước – Khó: Thay đổi mã nguồn, đánh dấu các lỗi; cố gắng nhớ xem đã thay đổi cái gì từ lần làm việc trước – Dễ hơn: Backup mã nguồn, thay đổi mã nguồn, đánh dấu các lỗi; so sánh phiên bản mới với phiên bản cũ để xác định các điểm thay đổi
  47. Tóm lại • Gỡ rối là 1 nghệ thuật mà ta phải luyện tập thường xuyên • Nhưng đó là nghệ thuật mà ta không muốn • Mã nguồn viết tốt có ít lỗi hơn và dễ tìm hơn • Đầu tiên phải nghĩ đến nguồn gốc sinh ra lỗi • Hãy suy nghĩ kỹ càng, có hệ thống để định vị khu vực gây lỗi • Không gì bằng học từ chính lỗi của mình – điều này càng đúng đối với LTV