Làm thế nào để bạn viết bài kiểm tra đơn vị cho mã với kết quả khó dự đoán?


124

Tôi thường xuyên làm việc với các chương trình số / toán học, trong đó kết quả chính xác của hàm rất khó dự đoán trước.

Khi thử áp dụng TDD với loại mã này, tôi thường thấy việc viết mã theo thử nghiệm dễ dàng hơn đáng kể so với viết thử nghiệm đơn vị cho mã đó, bởi vì cách duy nhất tôi biết để tìm kết quả mong đợi là áp dụng thuật toán (cho dù là của tôi đầu, trên giấy, hoặc bằng máy tính). Điều này cảm thấy sai, bởi vì tôi đang sử dụng hiệu quả mã được kiểm tra để xác minh các bài kiểm tra đơn vị của mình, thay vì cách khác.

Có các kỹ thuật đã biết để viết bài kiểm tra đơn vị và áp dụng TDD khi kết quả của mã được kiểm tra khó dự đoán không?

Một ví dụ (thực tế) về mã với kết quả khó dự đoán:

Một hàm weightedTasksOnTime, với một lượng công việc được thực hiện mỗi ngày workPerDaytrong phạm vi (0, 24], thời gian hiện tại initialTime> 0 và danh sách các nhiệm vụ taskArray, mỗi lần có một thời gian để hoàn thành thuộc tính time> 0, ngày đáo hạn duevà giá trị quan trọng importance; một giá trị được chuẩn hóa trong phạm vi [0, 1] thể hiện tầm quan trọng của các nhiệm vụ có thể hoàn thành trước duengày của chúng nếu mỗi tác vụ nếu được hoàn thành theo thứ tự được đưa ra bởi taskArray, bắt đầu từ initialTime.

Thuật toán để thực hiện chức năng này tương đối đơn giản: lặp qua các tác vụ trong taskArray. Đối với mỗi nhiệm vụ, thêm timevào initialTime. Nếu thời gian mới < due, hãy thêm importancevào một bộ tích lũy. Thời gian được điều chỉnh bởi công việc nghịch đảoPerDay. Trước khi trả lại bộ tích lũy, chia cho tổng số các nhiệm vụ quan trọng để bình thường hóa.

function weightedTasksOnTime(workPerDay, initialTime, taskArray) {
    let simulatedTime = initialTime
    let accumulator = 0;
    for (task in taskArray) {
        simulatedTime += task.time * (24 / workPerDay)
        if (simulatedTime < task.due) {
            accumulator += task.importance
        }
    }
    return accumulator / totalImportance(taskArray)
}

Tôi tin rằng vấn đề trên có thể được đơn giản hóa, trong khi vẫn duy trì cốt lõi của nó, bằng cách loại bỏ workPerDayvà yêu cầu chuẩn hóa, để đưa ra:

function weightedTasksOnTime(initialTime, taskArray) {
    let simulatedTime = initialTime
    let accumulator = 0;
    for (task in taskArray) {
        simulatedTime += task.time
        if (simulatedTime < task.due) {
            accumulator += task.importance
        }
    }
    return accumulator
}

Câu hỏi này giải quyết các tình huống trong đó mã được kiểm tra không phải là triển khai lại thuật toán hiện có. Nếu mã là một triển khai lại, về bản chất nó có thể dễ dàng dự đoán kết quả, bởi vì các triển khai đáng tin cậy hiện có của thuật toán hoạt động như một lời tiên tri thử nghiệm tự nhiên.


4
Bạn có thể cung cấp một ví dụ đơn giản về một hàm có kết quả khó dự đoán không?
Robert Harvey

62
FWIW bạn không kiểm tra thuật toán. Có lẽ đó là chính xác. Bạn đang thử nghiệm việc thực hiện. Làm việc bằng tay thường là tốt như một công trình song song.
Kristian H


7
Có những tình huống mà một thuật toán không thể được kiểm tra đơn vị một cách hợp lý - ví dụ nếu thời gian thực hiện của nó là nhiều ngày / tháng. Điều này có thể xảy ra khi giải quyết các vấn đề NP. Trong những trường hợp này, thể khả thi hơn khi cung cấp một bằng chứng chính thức rằng mã là chính xác.
Hulk

12
Một cái gì đó tôi đã thấy trong mã số rất phức tạp là chỉ coi các bài kiểm tra đơn vị là các bài kiểm tra hồi quy. Viết hàm, chạy nó cho một vài giá trị thú vị, xác nhận kết quả theo cách thủ công, sau đó viết bài kiểm tra đơn vị để bắt hồi quy từ kết quả mong đợi. Mã hóa kinh dị? Tò mò những gì người khác nghĩ.
Chuu

Câu trả lời:


251

Có hai điều bạn có thể kiểm tra trong mã khó kiểm tra. Đầu tiên, các trường hợp thoái hóa. Điều gì xảy ra nếu bạn không có phần tử nào trong mảng nhiệm vụ của mình, hoặc chỉ một, hoặc hai nhưng một là quá hạn, v.v ... Bất cứ điều gì đơn giản hơn vấn đề thực sự của bạn, nhưng vẫn hợp lý để tính toán thủ công.

Thứ hai là kiểm tra sự tỉnh táo. Đây là những kiểm tra bạn thực hiện khi bạn không biết câu trả lời có đúng không , nhưng bạn chắc chắn sẽ biết nếu nó sai . Đây là những thứ như thời gian phải tiến lên, giá trị phải nằm trong phạm vi hợp lý, tỷ lệ phần trăm phải thêm tới 100, v.v.

Vâng, điều này không tốt bằng một bài kiểm tra đầy đủ, nhưng bạn sẽ ngạc nhiên về mức độ thường xuyên bạn làm hỏng việc kiểm tra sự tỉnh táo và các trường hợp thoái hóa, điều đó cho thấy một vấn đề trong thuật toán đầy đủ của bạn.


54
Hãy nghĩ rằng đây là lời khuyên rất tốt. Bắt đầu bằng cách viết các loại bài kiểm tra đơn vị. Khi bạn phát triển phần mềm, nếu bạn tìm thấy lỗi hoặc câu trả lời không chính xác - hãy thêm chúng dưới dạng kiểm tra đơn vị. Làm tương tự, ở một mức độ nào đó, khi bạn tìm thấy câu trả lời chắc chắn chính xác. Xây dựng chúng theo thời gian và bạn (cuối cùng) sẽ có một bộ bài kiểm tra đơn vị rất hoàn chỉnh mặc dù bắt đầu không biết chúng sẽ ra sao ...
Algy Taylor

21
Một điều khác có thể hữu ích trong một số trường hợp (mặc dù có lẽ không phải cái này) là viết một hàm nghịch đảo và kiểm tra rằng, khi bị xiềng xích, đầu vào và đầu ra của bạn giống nhau.
Cyberspark

7
kiểm tra vệ sinh thường thực hiện các mục tiêu tốt cho các bài kiểm tra dựa trên tài sản với một cái gì đó như QuickCheck
jk.

10
một loại thử nghiệm khác mà tôi khuyên dùng là một vài loại để kiểm tra những thay đổi không chủ ý trong đầu ra. Bạn có thể 'gian lận' những điều này bằng cách sử dụng chính mã để tạo ra kết quả mong đợi vì mục đích của những điều này là giúp các nhà bảo trì bằng cách gắn cờ rằng một cái gì đó nhằm thay đổi trung lập đầu ra vô tình đã ảnh hưởng đến hành vi thuật toán.
Dan Neely

5
@iFlo Không chắc bạn có nói đùa không, nhưng nghịch đảo đã tồn tại. Đáng để nhận ra rằng thử nghiệm thất bại có thể là một vấn đề trong chức năng nghịch đảo
lucidbrot

80

Tôi đã từng viết các bài kiểm tra cho phần mềm khoa học với đầu ra khó dự đoán. Chúng tôi đã sử dụng rất nhiều mối quan hệ biến thái. Về cơ bản, có những điều bạn biết về cách phần mềm của bạn nên hoạt động ngay cả khi bạn không biết chính xác đầu ra số.

Một ví dụ có thể cho trường hợp của bạn: nếu bạn giảm số lượng công việc bạn có thể làm mỗi ngày thì tổng số lượng công việc bạn có thể làm sẽ tốt nhất là giữ nguyên, nhưng có khả năng giảm. Vì vậy, chạy hàm cho một số giá trị workPerDayvà đảm bảo mối quan hệ được giữ.


32
Quan hệ biến thái là một ví dụ cụ thể của thử nghiệm dựa trên tài sản , nói chung là một công cụ hữu ích cho các tình huống như thế này
Dannnno

38

Các câu trả lời khác có ý tưởng tốt để phát triển các bài kiểm tra cho trường hợp cạnh hoặc lỗi. Đối với những người khác, sử dụng thuật toán tự nó không lý tưởng (rõ ràng) nhưng vẫn hữu ích.

Nó sẽ phát hiện nếu thuật toán (hoặc dữ liệu phụ thuộc vào) đã thay đổi

Nếu thay đổi là một tai nạn, bạn có thể quay lại cam kết. Nếu thay đổi là có chủ ý, bạn cần xem lại bài kiểm tra đơn vị.


6
Và đối với hồ sơ, các loại thử nghiệm này thường được gọi là "thử nghiệm hồi quy" theo mục đích của chúng và về cơ bản là một mạng lưới an toàn cho bất kỳ sửa đổi / tái cấu trúc nào.
Pac0

21

Giống như cách bạn viết các bài kiểm tra đơn vị cho bất kỳ loại mã nào khác:

  1. Tìm một số trường hợp thử nghiệm đại diện, và thử nghiệm những trường hợp.
  2. Tìm trường hợp cạnh, và kiểm tra những trường hợp.
  3. Tìm các điều kiện lỗi và kiểm tra chúng.

Trừ khi mã của bạn liên quan đến một số yếu tố ngẫu nhiên hoặc không có tính xác định (nghĩa là nó sẽ không tạo ra cùng một đầu ra với cùng một đầu vào), nó là đơn vị có thể kiểm tra được.

Tránh tác dụng phụ, hoặc các chức năng bị ảnh hưởng bởi các lực bên ngoài. Các chức năng thuần túy dễ kiểm tra hơn.


2
Đối với các thuật toán không xác định, bạn có thể lưu hạt giống RNG hoặc giả định nó bằng cách sử dụng chuỗi xác định cố định hoặc chuỗi xác định sai lệch thấp, ví dụ chuỗi Halton
wonderra

14
@PaintingInAir Nếu không thể xác minh đầu ra của thuật toán, thuật toán có thể không chính xác không?
WolfgangGroiss

5
Unless your code involves some random elementMẹo ở đây là làm cho trình tạo số ngẫu nhiên của bạn trở thành một phụ thuộc được chèn, do đó bạn có thể thay thế nó cho trình tạo số mang lại kết quả chính xác mà bạn muốn. Điều này cho phép bạn kiểm tra lại chính xác - cũng đếm các số được tạo làm tham số đầu vào. not deterministic (i.e. it won't produce the same output given the same input)Vì một bài kiểm tra đơn vị nên bắt đầu từ một tình huống được kiểm soát , nó chỉ có thể không mang tính quyết định nếu nó có một yếu tố ngẫu nhiên - sau đó bạn có thể tiêm. Tôi không thể nghĩ đến những khả năng khác ở đây.
Flater

3
@PaintingInAir: Hoặc hoặc. Nhận xét của tôi áp dụng cho cả thực hiện nhanh hoặc viết thử nghiệm nhanh. Nếu bạn mất ba ngày để tính toán một ví dụ bằng tay (giả sử bạn sử dụng phương pháp nhanh nhất hiện có mà không sử dụng mã) - thì ba ngày là điều cần thiết. Thay vào đó, nếu bạn dựa vào kết quả thử nghiệm dự kiến ​​của mình trên chính mã thực tế, thì thử nghiệm đó đang tự thỏa hiệp. Điều đó giống như làm if(x == x), đó là một so sánh vô nghĩa. Bạn cần hai kết quả của bạn ( thực tế : đến từ mã; dự kiến : xuất phát từ kiến ​​thức bên ngoài của bạn) để độc lập với nhau.
Flater

2
Nó vẫn là đơn vị có thể kiểm tra được ngay cả khi không xác định, cung cấp cho nó tuân thủ các thông số kỹ thuật và có thể đo lường mức độ tuân thủ (ví dụ phân phối và lây lan ngẫu nhiên) Nó có thể chỉ cần rất nhiều mẫu để loại bỏ nguy cơ dị thường.
mckenzm

17

Cập nhật do bình luận được đăng

Câu trả lời ban đầu đã bị xóa vì lý do ngắn gọn - bạn có thể tìm thấy nó trong lịch sử chỉnh sửa.

PaintingInAir Đối với bối cảnh: là một doanh nhân và học thuật, hầu hết các thuật toán tôi thiết kế không được yêu cầu bởi bất kỳ ai khác ngoài tôi. Ví dụ được đưa ra trong câu hỏi là một phần của trình tối ưu hóa không có đạo hàm để tối đa hóa chất lượng của một đơn đặt hàng các nhiệm vụ. Về cách tôi mô tả sự cần thiết của chức năng ví dụ trong nội bộ: "Tôi cần một hàm mục tiêu để tối đa hóa tầm quan trọng của các nhiệm vụ được hoàn thành đúng hạn". Tuy nhiên, dường như vẫn còn một khoảng cách lớn giữa yêu cầu này và việc thực hiện các bài kiểm tra đơn vị.

Đầu tiên, một TL; DR để tránh câu trả lời dài dòng khác:

Hãy nghĩ về nó theo cách này:
Một khách hàng bước vào McDonald và yêu cầu một chiếc burger với rau diếp, cà chua và xà phòng rửa tay như toppings. Thứ tự này được trao cho đầu bếp, người làm cho bánh mì kẹp thịt chính xác theo yêu cầu. Khách hàng nhận được món burger này, ăn nó và sau đó phàn nàn với đầu bếp rằng đây không phải là một chiếc burger ngon!

Đây không phải là lỗi của đầu bếp - anh ta chỉ làm những gì khách hàng yêu cầu rõ ràng. Đây không phải là công việc của đầu bếp để kiểm tra xem đơn hàng được yêu cầu có thực sự ngon không . Đầu bếp chỉ đơn giản là tạo ra những gì mà khách hàng đặt hàng. Đó là trách nhiệm của khách hàng trong việc đặt hàng một cái gì đó mà họ thấy ngon .

Tương tự, đó không phải là công việc của nhà phát triển để đặt câu hỏi về tính chính xác của thuật toán. Công việc duy nhất của họ là thực hiện thuật toán theo yêu cầu.
Kiểm thử đơn vị là một công cụ của nhà phát triển. Nó xác nhận rằng burger phù hợp với đơn đặt hàng (trước khi nó rời khỏi bếp). Nó không (và không nên) cố gắng xác nhận rằng burger đã ra lệnh thực sự ngon.

Ngay cả khi bạn là cả khách hàng và đầu bếp, vẫn có một sự khác biệt có ý nghĩa giữa:

  • Tôi đã không chuẩn bị bữa ăn này đúng cách, nó không ngon (= lỗi nấu ăn). Một miếng bít tết bị cháy sẽ không bao giờ ngon, ngay cả khi bạn thích bít tết.
  • Tôi đã chuẩn bị bữa ăn đúng cách, nhưng tôi không thích nó (= lỗi khách hàng). Nếu bạn không thích bít tết, bạn sẽ không bao giờ thích ăn bít tết, ngay cả khi bạn nấu nó đến mức hoàn hảo.

Vấn đề chính ở đây là bạn không tạo ra sự tách biệt giữa khách hàng và nhà phát triển (và nhà phân tích - mặc dù vai trò đó cũng có thể được đại diện bởi một nhà phát triển).

Bạn cần phân biệt giữa kiểm tra mã và kiểm tra các yêu cầu nghiệp vụ.

Ví dụ, khách hàng muốn nó hoạt động như [này] . Tuy nhiên, nhà phát triển hiểu sai, và anh ta viết mã mà [đó] .

Do đó, nhà phát triển sẽ viết các bài kiểm tra đơn vị kiểm tra nếu [điều đó] hoạt động như mong đợi. Nếu anh ta phát triển ứng dụng một cách chính xác, các thử nghiệm đơn vị của anh ta sẽ vượt qua mặc dù ứng dụng không thực hiện [điều này] , điều mà khách hàng đang mong đợi.

Nếu bạn muốn kiểm tra kỳ vọng của khách hàng (các yêu cầu kinh doanh), điều đó cần được thực hiện trong một bước riêng (và sau đó).

Một quy trình phát triển đơn giản để cho bạn thấy khi nào các thử nghiệm này sẽ được chạy:

  • Khách hàng giải thích vấn đề họ muốn giải quyết.
  • Nhà phân tích (hoặc nhà phát triển) viết điều này trong một phân tích.
  • Nhà phát triển viết mã thực hiện những gì phân tích mô tả.
  • Nhà phát triển kiểm tra mã của anh ta (kiểm tra đơn vị) để xem anh ta có tuân theo phân tích chính xác không
  • Nếu thử nghiệm đơn vị thất bại, nhà phát triển quay lại phát triển. Vòng lặp này vô thời hạn, cho đến khi đơn vị kiểm tra tất cả vượt qua.
  • Bây giờ có một cơ sở mã được kiểm tra (xác nhận và thông qua), nhà phát triển xây dựng ứng dụng.
  • Ứng dụng được trao cho khách hàng.
  • Bây giờ khách hàng kiểm tra xem ứng dụng mà anh ta đưa ra có thực sự giải quyết được vấn đề mà anh ta tìm cách giải quyết hay không (kiểm tra QA) .

Bạn có thể tự hỏi ý nghĩa của việc thực hiện hai thử nghiệm riêng biệt khi khách hàng và nhà phát triển là một và giống nhau. Vì không có "bàn giao" từ nhà phát triển đến khách hàng, các bài kiểm tra được thực hiện lần lượt, nhưng chúng vẫn là các bước riêng biệt.

  • Kiểm tra đơn vị là một công cụ chuyên dụng giúp bạn xác minh xem giai đoạn phát triển của bạn đã kết thúc chưa.
  • Kiểm tra QA được thực hiện bằng cách sử dụng ứng dụng .

Nếu bạn muốn kiểm tra xem chính thuật toán của bạn có chính xác hay không, đó không phải là một phần công việc của nhà phát triển . Đó là mối quan tâm của khách hàng và khách hàng sẽ kiểm tra điều này bằng cách sử dụng ứng dụng.

Là một doanh nhân và học giả, bạn có thể đang thiếu một sự khác biệt quan trọng ở đây, trong đó nêu bật các trách nhiệm khác nhau.

  • Nếu ứng dụng không tuân thủ những gì khách hàng đã yêu cầu ban đầu, thì những thay đổi tiếp theo đối với mã thường được thực hiện miễn phí ; vì đó là lỗi của nhà phát triển. Nhà phát triển đã mắc lỗi và phải trả chi phí khắc phục nó.
  • Nếu ứng dụng thực hiện những gì khách hàng đã yêu cầu ban đầu, nhưng khách hàng hiện đã thay đổi quyết định (ví dụ: bạn đã quyết định sử dụng thuật toán khác và tốt hơn), các thay đổi đối với cơ sở mã được tính cho khách hàng , vì đó không phải là lỗi của nhà phát triển mà khách hàng yêu cầu một cái gì đó khác với những gì họ muốn bây giờ. Đó là trách nhiệm của khách hàng (chi phí) để thay đổi suy nghĩ của họ và do đó, các nhà phát triển phải dành nhiều nỗ lực hơn để phát triển một thứ mà trước đây không đồng ý.

Tôi sẽ rất vui khi thấy chi tiết hơn về tình huống "Nếu bạn tự mình nghĩ ra thuật toán", vì tôi nghĩ đây là tình huống có khả năng xảy ra vấn đề nhất. Đặc biệt trong các tình huống không có ví dụ "nếu A thì B, C khác" được cung cấp. (ps Tôi không phải là người downvoter)
PaintingInAir

@PaintingInAir: Nhưng tôi không thể thực sự giải thích về điều này vì nó phụ thuộc vào tình huống của bạn. Nếu bạn quyết định tạo thuật toán này, rõ ràng bạn đã làm như vậy để cung cấp một tính năng cụ thể. Ai yêu cầu bạn làm như vậy? Làm thế nào mà họ mô tả yêu cầu của họ? Họ có nói với bạn những gì họ cần phải xảy ra trong những tình huống nhất định không? (thông tin này là những gì tôi gọi là "phân tích" trong câu trả lời của tôi) Bất kỳ lời giải thích nào bạn nhận được (điều đó dẫn bạn tạo ra thuật toán) có thể được sử dụng để kiểm tra nếu thuật toán hoạt động theo yêu cầu. Nói tóm lại, bất cứ điều gì ngoại trừ thuật toán mã / tự tạo đều có thể được sử dụng.
Flater

2
@PaintingInAir: Thật nguy hiểm khi kết hợp chặt chẽ khách hàng, nhà phân tích và nhà phát triển; vì bạn có xu hướng bỏ qua các bước thiết yếu như xác định vấn đề ngay từ đầu . Tôi tin rằng đó là những gì bạn đang làm ở đây. Bạn dường như muốn kiểm tra tính chính xác của thuật toán, hơn là liệu nó có được thực hiện chính xác hay không. Nhưng đó không phải là cách bạn làm điều đó. Kiểm tra việc thực hiện có thể được thực hiện bằng cách sử dụng các bài kiểm tra đơn vị. Tự kiểm tra thuật toán là vấn đề sử dụng ứng dụng (đã kiểm tra) của bạn và kiểm tra thực tế kết quả của nó - thử nghiệm thực tế này nằm ngoài phạm vi của cơ sở mã của bạn (như nó phải vậy ).
Flater

4
Câu trả lời này đã rất lớn. Rất khuyến khích cố gắng tìm cách cải tổ nội dung ban đầu để bạn có thể tích hợp nó vào câu trả lời mới nếu bạn không muốn vứt nó đi.
jpmc26

7
Ngoài ra, tôi không đồng ý với tiền đề của bạn. Các thử nghiệm có thể và hoàn toàn sẽ tiết lộ khi mã tạo ra một đầu ra không chính xác theo đặc điểm kỹ thuật. Nó hợp lệ cho các thử nghiệm để xác nhận đầu ra cho một số trường hợp thử nghiệm đã biết. Ngoài ra, đầu bếp nên biết rõ hơn là chấp nhận "xà phòng rửa tay" như một thành phần bánh burger hợp lệ, và chủ nhân gần như chắc chắn đã dạy cho đầu bếp về những thành phần có sẵn.
jpmc26

9

Kiểm tra tài sản

Đôi khi các hàm toán học được phục vụ tốt hơn bởi "Kiểm tra thuộc tính" so với kiểm tra đơn vị dựa trên ví dụ truyền thống. Ví dụ, hãy tưởng tượng bạn đang viết các bài kiểm tra đơn vị cho một cái gì đó giống như một hàm "nhân" số nguyên. Mặc dù chức năng có vẻ rất đơn giản, nhưng nếu đó là cách duy nhất để nhân lên, làm thế nào để bạn kiểm tra kỹ lưỡng mà không có logic trong chính chức năng? Bạn có thể sử dụng các bảng khổng lồ với đầu vào / đầu ra dự kiến, nhưng điều này bị hạn chế và dễ bị lỗi.

Trong những trường hợp này, bạn có thể kiểm tra các thuộc tính đã biết của hàm, thay vì tìm kiếm các kết quả mong đợi cụ thể. Để nhân, bạn có thể biết rằng nhân một số âm và một số dương sẽ dẫn đến một số âm và việc nhân hai số âm sẽ dẫn đến một số dương, v.v ... Sử dụng các giá trị ngẫu nhiên và sau đó kiểm tra xem các thuộc tính này có được bảo toàn cho tất cả không kiểm tra giá trị là một cách tốt để kiểm tra các chức năng như vậy. Nói chung, bạn cần kiểm tra nhiều hơn một thuộc tính, nhưng bạn thường có thể xác định một tập các thuộc tính hữu hạn cùng xác thực hành vi đúng của hàm mà không nhất thiết phải biết kết quả mong đợi cho mọi trường hợp.

Một trong những lời giới thiệu tốt nhất về Kiểm tra tài sản mà tôi đã thấy là bài này trong F #. Hy vọng rằng cú pháp không phải là một cản trở để hiểu được lời giải thích của kỹ thuật.


1
Tôi sẽ đề nghị có thể thêm một cái gì đó cụ thể hơn một chút trong phép nhân lại ví dụ của bạn, chẳng hạn như tạo các bộ tứ ngẫu nhiên (a, b, c) và xác nhận rằng (ab) (cd) mang lại (ac-ad) - (bc-bd). Một hoạt động nhân có thể bị phá vỡ khá nhiều và vẫn duy trì quy tắc (thời gian âm mang lại kết quả âm), nhưng quy tắc phân phối dự đoán kết quả cụ thể.
supercat

4

Thật hấp dẫn khi viết mã và sau đó xem liệu kết quả "có đúng không", nhưng, như bạn đúng trực giác, đó không phải là một ý tưởng tốt.

Khi thuật toán khó, bạn có thể thực hiện một số điều để làm cho việc tính toán thủ công kết quả dễ dàng hơn.

  1. Sử dụng Excel. Thiết lập bảng tính thực hiện một số hoặc tất cả các phép tính cho bạn. Giữ nó đủ đơn giản để bạn có thể xem các bước.

  2. Chia phương pháp của bạn thành các phương thức có thể kiểm tra nhỏ hơn, mỗi phương thức có các bài kiểm tra riêng. Khi bạn chắc chắn các phần nhỏ hơn hoạt động, sử dụng chúng để làm việc thủ công qua bước tiếp theo.

  3. Sử dụng các thuộc tính tổng hợp để kiểm tra vệ sinh. Ví dụ, giả sử bạn có một máy tính xác suất; bạn có thể không biết kết quả cá nhân là gì, nhưng bạn biết tất cả chúng phải cộng tới 100%.

  4. Lực lượng vũ phu. Viết chương trình tạo ra tất cả các kết quả có thể và kiểm tra xem không có kết quả nào tốt hơn thuật toán của bạn tạo ra.


Đối với 3., không cho phép một số lỗi làm tròn ở đây. Có thể tổng số tiền của bạn lên tới 100,000001% hoặc các con số gần tương tự nhưng không chính xác.
Flater

2
Tôi không chắc lắm về 4. Nếu bạn có thể tạo ra kết quả tối ưu cho tất cả các kết hợp đầu vào có thể (mà sau đó bạn sử dụng để xác nhận thử nghiệm), thì bạn vốn đã có khả năng tính toán kết quả tối ưu và do đó không nên ' Không cần mã thứ hai này mà bạn đang cố kiểm tra. Tại thời điểm đó, bạn nên sử dụng trình tạo kết quả tối ưu hiện tại của mình vì nó đã được chứng minh là có hiệu quả. (và nếu nó chưa được chứng minh là có hiệu quả, thì bạn không thể dựa vào kết quả của nó để kiểm tra thực tế các bài kiểm tra của bạn để bắt đầu).
Flater

6
@flater thường bạn có những yêu cầu khác cũng như sự đúng đắn mà lực lượng vũ phu không đáp ứng. ví dụ hiệu suất.
Ewan

1
@flater Tôi ghét sử dụng sắp xếp của bạn, con đường ngắn nhất, động cơ cờ vua, vv nếu bạn tin điều đó. Nhưng id hoàn toàn đánh bạc trong lỗi làm tròn số của bạn cho phép sòng bạc cả ngày
Ewan

3
@flater bạn có từ chức khi bạn đến một trò chơi cầm đồ vua không? chỉ bởi vì toàn bộ trò chơi không thể bị ép buộc không có nghĩa là một vị trí không thể tin được. Chỉ vì bạn vũ phu buộc đường dẫn ngắn nhất chính xác đến một mạng không có nghĩa là bạn biết đường đi ngắn nhất trong tất cả các mạng
Ewan

2

TL; DR

Đi đến phần "kiểm tra so sánh" để được tư vấn không có trong các câu trả lời khác.


Bắt đầu

Bắt đầu bằng cách kiểm tra các trường hợp nên bị thuật toán loại bỏ ( workPerDayví dụ bằng 0 hoặc âm ) và các trường hợp không quan trọng (ví dụ tasksmảng trống ).

Sau đó, bạn muốn kiểm tra các trường hợp đơn giản nhất trước tiên. Đối với tasksđầu vào, chúng ta cần kiểm tra độ dài khác nhau; cần đủ để kiểm tra 0, 1 và 2 phần tử (2 thuộc danh mục "nhiều" cho bài kiểm tra này).

Nếu bạn có thể tìm thấy đầu vào có thể tính toán được, thì đó là một khởi đầu tốt. Một kỹ thuật đôi khi tôi sử dụng là bắt đầu từ một kết quả mong muốn và hoạt động trở lại (trong thông số kỹ thuật) cho các đầu vào sẽ tạo ra kết quả đó.

Kiểm tra so sánh

Đôi khi, mối quan hệ của đầu ra với đầu vào không rõ ràng, nhưng bạn có mối quan hệ có thể dự đoán được giữa các đầu ra khác nhau khi một đầu vào được thay đổi. Nếu tôi đã hiểu đúng ví dụ, thì việc thêm một tác vụ (không thay đổi các đầu vào khác) sẽ không bao giờ tăng tỷ lệ công việc được thực hiện đúng hạn, vì vậy chúng tôi có thể tạo một bài kiểm tra gọi hàm hai lần - một lần và một lần mà không cần thêm tác vụ - và khẳng định sự bất bình đẳng giữa hai kết quả.

Dự phòng

Đôi khi, tôi đã phải dùng đến một nhận xét dài cho thấy kết quả được tính toán bằng tay theo các bước tương ứng với thông số kỹ thuật (một nhận xét như vậy thường dài hơn trường hợp kiểm tra). Trường hợp xấu nhất là khi bạn phải duy trì khả năng tương thích với việc triển khai trước đó bằng một ngôn ngữ khác hoặc cho một môi trường khác. Đôi khi bạn chỉ cần dán nhãn dữ liệu thử nghiệm với một cái gì đó như /* derived from v2.6 implementation on ARM system */. Điều đó không thỏa mãn lắm, nhưng có thể được chấp nhận như một bài kiểm tra độ trung thực khi chuyển, hoặc như một cái nạng ngắn hạn.

Nhắc nhở

Thuộc tính quan trọng nhất của kiểm tra là khả năng đọc của nó - nếu đầu vào và đầu ra mờ đối với người đọc, thì thử nghiệm có giá trị rất thấp, nhưng nếu người đọc được giúp hiểu các mối quan hệ giữa chúng, thì thử nghiệm phục vụ hai mục đích.

Đừng quên sử dụng "xấp xỉ bằng" cho kết quả không chính xác (ví dụ: dấu phẩy động).

Tránh kiểm tra quá mức - chỉ thêm một thử nghiệm nếu nó bao gồm một cái gì đó (chẳng hạn như giá trị biên) mà các thử nghiệm khác không đạt được.


2

Không có gì đặc biệt về loại chức năng khó kiểm tra này. Điều tương tự cũng áp dụng cho mã sử dụng giao diện bên ngoài (giả sử API REST của ứng dụng bên thứ 3 không thuộc quyền kiểm soát của bạn và chắc chắn không được kiểm tra bởi bộ thử nghiệm của bạn; hoặc sử dụng thư viện bên thứ 3 mà bạn không chắc chắn về định dạng byte chính xác của các giá trị trả về).

Đó là một cách tiếp cận khá hợp lệ để đơn giản chạy thuật toán của bạn cho một số đầu vào lành mạnh, xem nó làm gì, đảm bảo rằng kết quả là chính xác và đóng gói đầu vào và kết quả như một trường hợp thử nghiệm. Bạn có thể làm điều này trong một vài trường hợp và do đó có được một số mẫu. Cố gắng làm cho các tham số đầu vào khác nhau nhất có thể. Trong trường hợp có lệnh gọi API bên ngoài, bạn sẽ thực hiện một vài cuộc gọi với hệ thống thực, theo dõi chúng bằng một số công cụ và sau đó giả chúng vào các bài kiểm tra đơn vị của bạn để xem chương trình của bạn phản ứng như thế nào - giống như chỉ chọn một vài chạy mã kế hoạch nhiệm vụ của bạn, xác minh chúng bằng tay và sau đó mã hóa kết quả trong các thử nghiệm của bạn.

Sau đó, rõ ràng, mang lại các trường hợp cạnh như (trong ví dụ của bạn) một danh sách các nhiệm vụ trống; đại loại như thế.

Bộ kiểm tra của bạn có thể sẽ không tuyệt vời như đối với một phương pháp mà bạn có thể dễ dàng dự đoán kết quả; nhưng vẫn tốt hơn 100% so với không có bộ thử nghiệm (hoặc chỉ là thử nghiệm khói).

Tuy nhiên, nếu vấn đề của bạn là bạn cảm thấy khó quyết định liệu kết quả đúng hay không, thì đó là một vấn đề hoàn toàn khác. Ví dụ: giả sử bạn có một phương pháp phát hiện xem một số lớn tùy ý có phải là số nguyên tố hay không. Bạn khó có thể ném bất kỳ số ngẫu nhiên nào vào nó và sau đó chỉ "nhìn" nếu kết quả là chính xác (giả sử bạn không thể quyết định số nguyên tố trong đầu hoặc trên một tờ giấy). Trong trường hợp này, thực sự có rất ít bạn có thể làm - bạn cần có được kết quả đã biết (nghĩa là một số số nguyên tố lớn) hoặc thực hiện chức năng với một thuật toán khác (thậm chí có thể là một nhóm khác - NASA dường như rất thích điều đó) và hy vọng rằng nếu một trong hai thực hiện là có lỗi, thì ít nhất lỗi không dẫn đến kết quả sai tương tự.

Nếu đây là một trường hợp thường xuyên cho bạn, thì bạn phải có một cuộc nói chuyện khó khăn với các kỹ sư yêu cầu của bạn. Nếu họ không thể đưa ra các yêu cầu của bạn theo cách dễ dàng (hoặc hoàn toàn có thể) để kiểm tra cho bạn, thì khi nào bạn biết liệu bạn đã kết thúc chưa?


2

Các câu trả lời khác là tốt, vì vậy tôi sẽ cố gắng nhấn vào một số điểm mà họ đã bỏ lỡ chung cho đến nay.

Tôi đã viết phần mềm (và đã kiểm tra kỹ lưỡng) để xử lý hình ảnh bằng cách sử dụng Radar khẩu độ tổng hợp (SAR). Đó là tính khoa học / số trong tự nhiên (có rất nhiều hình học, vật lý và toán học liên quan).

Một vài lời khuyên (để kiểm tra khoa học / số học nói chung):

1) Sử dụng nghịch đảo. Có gì các fftcủa [1,2,3,4,5]? Không ý kiến. Có gì ifft(fft([1,2,3,4,5]))? Nên [1,2,3,4,5](hoặc gần với nó, lỗi dấu phẩy động có thể xuất hiện). Tương tự với trường hợp 2D.

2) Sử dụng các khẳng định đã biết. Nếu bạn viết một hàm xác định, có thể khó có thể nói định thức nào là ma trận 100x100 ngẫu nhiên. Nhưng bạn có biết rằng yếu tố quyết định của ma trận danh tính là 1, ngay cả khi đó là 100x100. Bạn cũng biết rằng hàm sẽ trả về 0 trên ma trận không thể đảo ngược (như 100x100 có đầy đủ tất cả 0).

3) Sử dụng các xác nhận thô thay vì các xác nhận chính xác. Tôi đã viết một số mã để xử lý SAR cho biết sẽ đăng ký hai hình ảnh bằng cách tạo các điểm liên kết tạo ra ánh xạ giữa các hình ảnh và sau đó thực hiện một sợi dọc giữa chúng để làm cho chúng khớp với nhau. Nó có thể đăng ký ở cấp độ pixel phụ. Một tiên nghiệm, thật khó để nói bất cứ điều gì về việc đăng ký hai hình ảnh có thể trông như thế nào. Làm thế nào bạn có thể kiểm tra nó? Những thứ như:

EXPECT_TRUE(register(img1, img2).size() < min(img1.size(), img2.size()))

vì bạn chỉ có thể đăng ký trên các phần chồng chéo, hình ảnh đã đăng ký phải nhỏ hơn hoặc bằng với hình ảnh nhỏ nhất của bạn và cũng:

scale = 255
EXPECT_PIXEL_EQ_WITH_TOLERANCE(reg(img, img), img, .05*scale)

do một hình ảnh được đăng ký với chính nó phải ĐÓNG với chính nó, nhưng bạn có thể gặp nhiều hơn một chút so với các lỗi dấu phẩy động do thuật toán trong tay, vì vậy chỉ cần kiểm tra mỗi pixel nằm trong khoảng +/- 5% phạm vi mà các pixel có thể đảm nhận (0-255 là thang độ xám, phổ biến trong xử lý ảnh). Kết quả ít nhất phải có cùng kích thước với đầu vào.

Bạn thậm chí có thể chỉ cần kiểm tra khói (tức là gọi nó và đảm bảo nó không bị sập). Nói chung, kỹ thuật này tốt hơn cho các thử nghiệm lớn hơn trong đó kết quả cuối cùng không thể (dễ dàng) tính toán tiên nghiệm để chạy thử nghiệm.

4) Sử dụng HOẶC LƯU TRỮ hạt giống số ngẫu nhiên cho RNG của bạn.

Chạy làm cần phải được tái sản xuất. Tuy nhiên, điều sai lầm là cách duy nhất để có được một lần chạy có thể lặp lại là cung cấp một hạt giống cụ thể cho một trình tạo số ngẫu nhiên. Đôi khi thử nghiệm ngẫu nhiên là có giá trị. Tôi đã thấy / nghe về các lỗi trong mã khoa học mọc lên trong các trường hợp suy biến được tạo ngẫu nhiên (trong các thuật toán phức tạp, thật khó để biết trường hợp suy biến là gì). Thay vì luôn gọi hàm của bạn bằng cùng một hạt giống, hãy tạo một hạt giống ngẫu nhiên, sau đó sử dụng hạt giống đó và ghi lại giá trị của hạt giống đó. Theo cách đó, mỗi lần chạy có một hạt giống ngẫu nhiên khác nhau, nhưng nếu bạn gặp sự cố, bạn có thể chạy lại kết quả bằng cách sử dụng hạt giống mà bạn đã đăng nhập để gỡ lỗi. Tôi thực sự đã sử dụng nó trong thực tế và nó đã khắc phục một lỗi, vì vậy tôi đoán rằng tôi đã đề cập đến nó. Phải thừa nhận rằng điều này chỉ xảy ra một lần và tôi khẳng định không phải lúc nào cũng đáng làm, vì vậy hãy sử dụng kỹ thuật này một cách thận trọng. Tuy nhiên, ngẫu nhiên với cùng một hạt giống luôn luôn an toàn. Nhược điểm (trái ngược với việc chỉ sử dụng cùng một hạt giống mọi lúc): Bạn phải ghi nhật ký chạy thử. Ưu điểm: Chính xác và sửa lỗi.

Trường hợp cụ thể của bạn

1) Kiểm tra rằng một trả về trống taskArray 0 (xác nhận đã biết).

2) Tạo đầu vào ngẫu nhiên như vậy task.time > 0 , task.due > 0, task.importance > 0 cho tất cả các task s, và khẳng định kết quả là lớn hơn 0 (assert thô, đầu vào ngẫu nhiên) . Bạn không cần phải phát điên và tạo ra các hạt ngẫu nhiên, thuật toán của bạn không đủ phức tạp để đảm bảo nó. Có khoảng 0 cơ hội nó sẽ được đền đáp: chỉ cần giữ cho bài kiểm tra đơn giản.

3) Kiểm tra nếu task.importance == 0 cho tất cả task s, thì kết quả là 0 (xác nhận đã biết)

4) Các câu trả lời khác đã chạm vào điều này, nhưng nó có thể quan trọng đối với trường hợp cụ thể của bạn : Nếu bạn đang tạo một API để người dùng bên ngoài nhóm của bạn sử dụng, bạn cần kiểm tra các trường hợp thoái hóa. Ví dụ: nếu workPerDay == 0, hãy đảm bảo bạn đưa ra một lỗi đáng yêu cho người dùng biết rằng đầu vào không hợp lệ. Nếu bạn không tạo API và nó chỉ dành cho bạn và nhóm của bạn, bạn có thể bỏ qua bước này và chỉ từ chối gọi nó với trường hợp thoái hóa.

HTH.


1

Kết hợp kiểm tra xác nhận vào bộ kiểm thử đơn vị của bạn để kiểm tra dựa trên thuộc tính của thuật toán của bạn. Ngoài việc viết các bài kiểm tra đơn vị kiểm tra đầu ra cụ thể, hãy viết các bài kiểm tra được thiết kế để thất bại bằng cách kích hoạt các lỗi xác nhận trong mã chính.

Nhiều thuật toán dựa trên bằng chứng chính xác của chúng về việc duy trì các thuộc tính nhất định trong suốt các giai đoạn của thuật toán. Nếu bạn có thể kiểm tra một cách hợp lý các thuộc tính này bằng cách nhìn vào đầu ra của hàm, chỉ riêng việc kiểm tra đơn vị là đủ để kiểm tra các thuộc tính của bạn. Mặt khác, kiểm tra dựa trên khẳng định cho phép bạn kiểm tra rằng việc triển khai duy trì một thuộc tính mỗi khi thuật toán giả định nó.

Thử nghiệm dựa trên khẳng định sẽ phơi bày các lỗ hổng thuật toán, lỗi mã hóa và lỗi thực thi do các vấn đề như mất ổn định số. Nhiều ngôn ngữ có các xác nhận dải cơ chế tại thời điểm biên dịch hoặc trước khi mã được diễn giải để khi chạy trong chế độ sản xuất, các xác nhận đó không bị phạt hiệu suất. Nếu mã của bạn vượt qua các bài kiểm tra đơn vị nhưng không thành công trong trường hợp thực tế, bạn có thể bật lại các xác nhận dưới dạng công cụ gỡ lỗi.


1

Một số câu trả lời khác ở đây rất hay:

  • Kiểm tra cơ sở, cạnh và trường hợp góc
  • Thực hiện kiểm tra vệ sinh
  • Thực hiện các bài kiểm tra so sánh

... Tôi sẽ thêm một vài chiến thuật khác:

  • Phân tích vấn đề.
  • Chứng minh thuật toán ngoài mã.
  • Kiểm tra xem thuật toán [đã được chứng minh bên ngoài] được triển khai như thiết kế.

Phân tách cho phép bạn đảm bảo các thành phần của thuật toán của bạn làm những gì bạn mong đợi chúng làm. Và phân tách "tốt" cho phép bạn cũng đảm bảo chúng được dán với nhau đúng cách. Một phân tách lớn tổng quát hóa và đơn giản hóa thuật toán đến mức bạn có thể dự đoán kết quả (của thuật toán chung, đơn giản hóa) bằng tay đủ tốt để viết các bài kiểm tra kỹ lưỡng.

Nếu bạn không thể phân tách đến mức đó, hãy chứng minh thuật toán bên ngoài mã bằng bất kỳ phương tiện nào đủ để đáp ứng cho bạn và các đồng nghiệp, các bên liên quan và khách hàng của bạn. Và sau đó, chỉ cần phân tách đủ để chứng minh việc thực hiện của bạn phù hợp với thiết kế.


0

Điều này có vẻ giống như một câu trả lời duy tâm nhưng nó giúp xác định các loại thử nghiệm khác nhau.

Nếu câu trả lời nghiêm ngặt là quan trọng đối với việc thực hiện thì các ví dụ và câu trả lời dự kiến ​​thực sự cần được cung cấp trong các yêu cầu mô tả thuật toán. Các yêu cầu này phải được xem xét theo nhóm và nếu bạn không nhận được kết quả tương tự, lý do cần được xác định.

Ngay cả khi bạn đang đóng vai trò là nhà phân tích cũng như người thực hiện, bạn thực sự nên tạo ra các yêu cầu và xem xét chúng từ lâu trước khi bạn viết bài kiểm tra đơn vị, vì vậy trong trường hợp này bạn sẽ biết kết quả mong đợi và có thể viết bài kiểm tra của mình cho phù hợp.

Mặt khác, nếu đây là một phần bạn đang triển khai không phải là một phần của logic nghiệp vụ hoặc hỗ trợ câu trả lời logic nghiệp vụ thì bạn nên chạy thử để xem kết quả là gì và sau đó sửa đổi thử nghiệm để mong đợi những kết quả đó. Các kết quả cuối cùng đã được kiểm tra theo yêu cầu của bạn, vì vậy nếu chúng là chính xác thì tất cả các mã cho các kết quả cuối đó phải chính xác về mặt số lượng và tại thời điểm đó, các thử nghiệm đơn vị của bạn sẽ phát hiện ra các trường hợp lỗi cạnh và thay đổi cấu trúc lại trong tương lai hơn là để chứng minh rằng thuật toán tạo ra kết quả chính xác.


0

Tôi nghĩ rằng nó hoàn toàn chấp nhận được vào các dịp để tuân theo quy trình:

  • thiết kế một trường hợp thử nghiệm
  • sử dụng phần mềm của bạn để có câu trả lời
  • kiểm tra câu trả lời bằng tay
  • viết một bài kiểm tra hồi quy để các phiên bản phần mềm trong tương lai sẽ tiếp tục đưa ra câu trả lời này.

Đây là một cách tiếp cận hợp lý trong mọi tình huống trong đó việc kiểm tra tính chính xác của câu trả lời bằng tay dễ dàng hơn so với việc tính toán câu trả lời bằng tay từ các nguyên tắc đầu tiên.

Tôi biết những người viết phần mềm để hiển thị các trang in và có các bài kiểm tra kiểm tra chính xác các pixel phù hợp được đặt trên trang in. Cách duy nhất để làm điều đó là viết mã để hiển thị trang, kiểm tra bằng mắt xem nó có tốt không, và sau đó chụp kết quả dưới dạng thử nghiệm hồi quy cho các bản phát hành trong tương lai.

Chỉ vì bạn đọc trong một cuốn sách mà một phương pháp cụ thể khuyến khích viết các trường hợp kiểm tra trước, không có nghĩa là bạn luôn phải làm theo cách đó. Các quy tắc là có để được phá vỡ.


0

Các câu trả lời khác đã có các kỹ thuật cho một bài kiểm tra trông như thế nào khi kết quả cụ thể không thể được xác định bên ngoài chức năng được kiểm tra.

Những gì tôi làm thêm mà tôi không phát hiện ra trong các câu trả lời khác là tự động tạo các bài kiểm tra theo một cách nào đó:

  1. Đầu vào 'ngẫu nhiên'
  2. Lặp lại trên phạm vi dữ liệu
  3. Xây dựng các trường hợp thử nghiệm từ các bộ ranh giới
  4. Tất cả những điều trên.

Ví dụ: nếu hàm lấy ba tham số, mỗi tham số có phạm vi đầu vào được phép [-1,1], hãy kiểm tra tất cả các kết hợp của từng tham số, {-2, -1,01, -1, -0,99, -0,5, -0,01, 0,0,01 , 0,5,0,99,1,1,01,2, một số ngẫu nhiên hơn trong (-1,1)}

Tóm lại: Đôi khi chất lượng kém có thể được trợ cấp theo số lượng.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.