Bài 31

Kiểm thử và gỡ lỗi chương trình

Biết được một vài phương pháp đơn giản kiểm thử chương trình, biết được một vài cách gỡ lỗi đơn giản một chương trình.

🚀

1. Khởi động (Engagement)

Bài học trước em đã biết khái niệm lỗi ngoại lệ khi chạy chương trình Python. Tuy nhiên, một chương trình chạy không có lỗi ngoại lệ (chương trình không bị dừng) thì không có nghĩa là chương trình không có lỗi. Thậm chí các "lỗi" không tường minh này (các lỗi này được gọi bug) càng khó phát hiện và khó sửa.

💡 Câu hỏi suy ngẫm: Theo em, làm thế nào để kiểm tra (test) và gỡ lỗi (debug) một chương trình? Môi trường lập trình có công cụ nào hỗ trợ việc đó không?

Trong bài học này, chúng ta sẽ tìm hiểu các phương pháp và công cụ để kiểm thử chương trình, giúp phát hiện và sửa lỗi hiệu quả hơn.

📖

2. Hình thành kiến thức (Knowledge)

1. Một vài phương pháp kiểm thử chương trình

Có rất nhiều phương pháp và công cụ khác nhau để kiểm thử chương trình. Các công cụ này không những có mục đích tìm ra lỗi (hay bug) của chương trình mà còn có tác dụng phòng ngừa và ngăn chặn các lỗi phát sinh tiếp trong tương lai.

a) Quan sát mã lỗi Runtime và bắt lỗi ngoại lệ

Nếu chương trình có lỗi Runtime (tức là đang chạy bị dừng lại), cần quan sát các mã lỗi (mã lỗi ngoại lệ) để kiểm tra vị trí dòng lệnh sinh ra lỗi này. Từ đó phân tích, tìm và sửa lỗi.

b) Kiểm thử chương trình với các bộ dữ liệu test

Chương trình cần được thử với một số bộ dữ liệu test gồm đầu vào tiêu biểu phụ thuộc đặc thù của bài toán và kết quả đầu ra đã biết trước. Các bộ test có thể có đầu vào theo các tiêu chí khác nhau như độ lớn và tính đa dạng của dữ liệu.

Cần chú ý một số điểm sau:

  • Cần có nhiều bộ test (theo các tiêu chí khác nhau như độ lớn, tính đa dạng của dữ liệu,...)
  • Cần có bộ test ngẫu nhiên. Việc sinh ngẫu nhiên dữ liệu đầu vào trong miền xác định của chương trình làm tăng khả năng tìm lỗi nếu có
  • Cần có bộ test dữ liệu ở vùng biên. Ví dụ dữ liệu đầu vào là cặp (x, y) xác định trên miền 0 ≤ x, y ≤ 1. Khi đó cần kiểm tra chương trình với bộ dữ liệu biên là (0; 0), (0; 1), (1; 0) và (1; 1). Thực tế cho thấy thường phát sinh lỗi tại các vùng biên hoặc lân cận của biên. Một ví dụ khác của dữ liệu biên là cần tìm các bộ test với n và các giá trị (x₁, x₂,..., xₙ) rất lớn (vùng cận biên lớn)

c) In các thông số trung gian

In các thông số trung gian ra màn hình để theo dõi giá trị của các biến tại các thời điểm khác nhau trong quá trình thực hiện chương trình. Đây là cách đơn giản nhưng hiệu quả để kiểm tra dòng chảy của chương trình và tìm ra các lỗi logic.

2. Một số lỗi ngoại lệ thường gặp

Chúng ta đã biết, nếu gặp lỗi ngoại lệ, chương trình Python sẽ dừng lại và báo lỗi. Một trong những vấn đề được đưa ra khi kiểm soát lỗi là làm thế nào để vẫn phát hiện lỗi, xử lí lỗi nhưng chương trình không bị dừng lại trong khi thực hiện.

Bảng các lỗi ngoại lệ thường gặp:

Mã lỗi ngoại lệMô tả lỗi
ZeroDivisionErrorLỗi này xảy ra khi lệnh thực hiện phép chia cho giá trị 0
IndexErrorLỗi xảy ra khi lệnh cố gắng truy cập phần tử của danh sách nhưng chỉ số vượt quá giới hạn
NameErrorLỗi xảy ra khi chương trình muốn tìm một tên nhưng không thấy. Ví dụ khi lệnh gọi một hàm nhưng không có hàm đó
TypeErrorLỗi kiểu dữ liệu. Một số ví dụ lỗi loại này: lệnh truy cập một phần tử của danh sách nhưng chỉ số không là số nguyên; lệnh tính biểu thức số nhưng lại có một toán hạng không phải là số
ValueErrorLỗi liên quan đến giá trị của đối tượng. Lỗi khi thực hiện lệnh chuyển đổi kiểu dữ liệu, đối số của hàm có giá trị mà hàm không hỗ trợ. Ví dụ khi thực hiện lệnh int("1.55") sẽ sinh lỗi loại này
IndentationErrorLỗi khi các dòng lệnh thụt vào không thẳng hàng hoặc không đúng vị trí
SyntaxErrorLỗi cú pháp
🧩

3. Luyện tập (Practice)

Bài tập thực hành

Bài tập 1: Nhận biết lỗi ngoại lệ

Các lệnh sau có sinh lỗi chương trình không? Nếu có thì mã lỗi là gì?

a)

A = [1, 3, 5, 10, 0]
for k in range(1, len(A) + 1):
    print(A[k])
Python Script

Phân tích:

  • Danh sách A có 5 phần tử (chỉ số 0-4)
  • Vòng lặp chạy từ 1 đến 5 (len(A) + 1 = 6)
  • Khi k = 5, A[5] sẽ gây ra IndexError

b)

s1, s2 = "101010", 1010101
s = s1 + s2
Python Script

Phân tích:

  • Không có lỗi, Python tự động chuyển số 1010101 thành xâu khi nối với xâu

Bài tập 2: Phân tích lỗi có thể xảy ra

Để tính giá trị trung bình của một danh sách số A, người lập trình đã dùng lệnh sau để tính:

gttb = sum(A)/len(A)
Python Script

Lệnh này có thể sinh lỗi ngoại lệ không? Nếu có thì là những lỗi gì?

Phân tích các lỗi có thể xảy ra:

  1. ZeroDivisionError:
  • Khi danh sách A rỗng (len(A) = 0)
  • sum(A) = 0, len(A) = 0 → chia cho 0
  1. TypeError:
  • Khi danh sách A chứa các phần tử không phải số
  • Ví dụ: A = [1, 2, "abc", 3]
  • sum(A) không thể thực hiện được
  1. NameError:
  • Khi biến A chưa được định nghĩa

Bài tập 3: Xác định mã lỗi

Hãy nêu mã lỗi ngoại lệ của mỗi lệnh sau nếu xảy ra lỗi:

a) A[1.5] → TypeError (chỉ số phải là số nguyên)

b) int("abc") → ValueError (không thể chuyển "abc" thành số nguyên)

c) "10"*3.5 → TypeError (không thể nhân xâu với số thực)

d) 12 + x(10) → TypeError (x không phải là hàm hoặc không được định nghĩa)

Thực hành tại máy:

Viết chương trình nhập các số nguyên m, n từ bàn phím, cách nhau bởi dấu cách. Chương trình đưa ra tổng, hiệu, thương của hai số đã nhập.

Chương trình mẫu:

s = input("Nhập hai số m, n cách nhau bởi dấu cách: ")
sline = s.split()
m, n = int(sline[0]), int(sline[1])
print("Tổng, hiệu, thương 2 số đã nhập là:", m+n, m-n, m/n)
Python Script

Các khả năng sinh lỗi của chương trình:

  • Các số m, n khi nhập vào không là số nguyên → ValueError
  • Giữa hai số m, n không có dấu cách → IndexError
  • Số n nhập vào là số 0 → ZeroDivisionError
🌟

4. Vận dụng (Application)

Bài tập vận dụng

Bài tập 1: Chương trình tìm ước số với kiểm thử

Giả sử em được yêu cầu viết một chương trình nhập số tự nhiên n từ bàn phím, kết quả đưa ra là danh sách các ước số thực sự của n, tính cả 1 và không tính n. Hãy viết chương trình và kiểm tra các khả năng sinh lỗi khi thực hiện chương trình.

Chương trình mẫu:

def tim_uoc_so(n):
    uoc_so = []
    for i in range(1, n):
        if n % i == 0:
            uoc_so.append(i)
    return uoc_so

try:
    n = int(input("Nhập số tự nhiên n: "))
    if n <= 1:
        print("Số phải lớn hơn 1 để có ước số thực sự!")
    else:
        ds_uoc = tim_uoc_so(n)
        print(f"Các ước số thực sự của {n}: {ds_uoc}")
except ValueError:
    print("Vui lòng nhập số nguyên!")
except Exception as e:
    print(f"Đã xảy ra lỗi: {e}")
Python Script

Kiểm thử các trường hợp:

  • Dữ liệu biên: n = 1, n = 2
  • Dữ liệu bất thường: n = 0, n = -5, nhập "abc"
  • Dữ liệu thông thường: n = 10, n = 100

Bài tập 2: Tạo chương trình sinh lỗi NameError

Em hãy viết một chương trình nhỏ để khi chạy sẽ sinh mã lỗi NameError.

Chương trình mẫu:

# Chương trình sinh lỗi NameError
print(x)  # x chưa được định nghĩa

# Hoặc:
def tinh_tong(a, b):
    return a + c  # c chưa được định nghĩa

tinh_tong(1, 2)
Python Script

Bài tập 3: Chương trình nhập danh sách số với kiểm thử

Viết chương trình nhập số tự nhiên n và nhập lần lượt n số nguyên đưa vào danh sách số A. Sau khi nhập xong in danh sách A ra màn hình.

Chương trình mẫu với xử lý lỗi:

def nhap_danh_sach():
    try:
        n = int(input("Nhập số tự nhiên n: "))
        if n <= 0:
            print("Số phải nguyên dương!")
            return []
        
        A = []
        for k in range(n):
            while True:
                try:
                    num = int(input(f"Nhập số thứ {k+1}: "))
                    A.append(num)
                    break
                except ValueError:
                    print("Vui lòng nhập số nguyên!")
        
        return A
    except ValueError:
        print("Vui lòng nhập số nguyên cho n!")
        return []
    except Exception as e:
        print(f"Đã xảy ra lỗi: {e}")
        return []

# Chương trình chính
danh_sach = nhap_danh_sach()
if danh_sach:
    print("Dãy đã nhập:", danh_sach)

# In thông số trung gian để kiểm tra
print(f"Số phần tử: {len(danh_sach)}")
print(f"Tổng các phần tử: {sum(danh_sach)}")
Python Script

Thảo luận:

  • Tại sao cần kiểm thử chương trình với nhiều bộ dữ liệu khác nhau?
  • Lợi ích của việc in thông số trung gian khi gỡ lỗi là gì?
  • Khi nào nên dùng try...except và khi nào nên dùng if...else để kiểm tra lỗi?

Ghi nhớ kiến thức 🎯

  • ✅ Kiểm thử: dùng các bộ test để tìm lỗi trong chương trình.
  • ✅ Dữ liệu biên: các giá trị ranh giới dễ gây lỗi nhất.
  • ✅ In thông số trung gian: cách đơn giản để theo dõi dòng chảy chương trình.
  • ✅ ZeroDivisionError: lỗi chia cho 0.
  • ✅ IndexError: lỗi truy cập chỉ số ngoài phạm vi.
  • ✅ ValueError: lỗi chuyển đổi kiểu dữ liệu.
  • ✅ Kiểm thử giúp chương trình đáng tin cậy hơn.

Chúc mừng bạn đã hoàn thành bài học này! 🏆