Các bài kiểm tra đơn vị tốt để bao gồm các trường hợp sử dụng cán chết là gì?


18

Tôi đang cố gắng để nắm bắt với thử nghiệm đơn vị.

Giả sử chúng ta có một con súc sắc có thể có số cạnh mặc định bằng 6 (nhưng có thể là 4, 5 mặt, v.v.):

import random
class Die():
    def __init__(self, sides=6):
        self._sides = sides

    def roll(self):
        return random.randint(1, self._sides)

Điều sau đây sẽ là kiểm tra đơn vị hợp lệ / hữu ích?

  • kiểm tra một cuộn trong phạm vi 1-6 cho một khuôn 6 mặt
  • kiểm tra một cuộn 0 cho một khuôn 6 mặt
  • kiểm tra một cuộn 7 cho một khuôn 6 mặt
  • kiểm tra một cuộn trong phạm vi 1-3 cho một khuôn 3 mặt
  • kiểm tra một cuộn 0 cho một khuôn 3 mặt
  • kiểm tra một cuộn 4 cho một khuôn 3 mặt

Tôi chỉ nghĩ rằng đây là một sự lãng phí thời gian vì mô-đun ngẫu nhiên đã tồn tại đủ lâu nhưng sau đó tôi nghĩ rằng nếu mô-đun ngẫu nhiên được cập nhật (giả sử tôi cập nhật phiên bản Python của tôi) thì ít nhất tôi sẽ được bảo vệ.

Ngoài ra, tôi thậm chí có cần phải kiểm tra các biến thể khác của cuộn chết không, ví dụ như 3 trong trường hợp này, hay nó có tốt để bao phủ một trạng thái chết khởi tạo khác không?


1
Điều gì về một cái chết trừ 5 mặt, hoặc một cái chết không có giá trị?
JensG

Câu trả lời:


22

Bạn đã đúng, các bài kiểm tra của bạn không nên xác minh rằng randommô-đun đang thực hiện công việc của nó; một unittest chỉ nên kiểm tra chính lớp đó, chứ không phải cách nó tương tác với mã khác (cần được kiểm tra riêng).

Tất nhiên là hoàn toàn có thể mã của bạn sử dụng random.randint()sai; hoặc bạn đang gọi random.randrange(1, self._sides)thay vào đó và cái chết của bạn không bao giờ ném giá trị cao nhất, nhưng đó là một loại lỗi khác, không phải là lỗi mà bạn có thể mắc phải với một lỗi nhỏ nhất. Trong trường hợp đó, die đơn vị của bạn đang làm việc như thiết kế, nhưng bản thân thiết kế đã bị lỗi.

Trong trường hợp này, tôi muốn sử dụng chế giễu để thay thế các randint()chức năng, và chỉ xác nhận rằng nó đã được gọi một cách chính xác. Python 3.3 trở lên đi kèm với unittest.mockmô-đun để xử lý loại thử nghiệm này, nhưng bạn có thể cài đặt mockgói bên ngoài trên các phiên bản cũ hơn để có được chức năng chính xác tương tự

import unittest
try:
    from unittest.mock import patch
except ImportError:
    # < python 3.3
    from mock import patch


@patch('random.randint', return_value=3)
class TestDice(unittest.TestCase):
    def _make_one(self, *args, **kw):
        from die import Die
        return Die(*args, **kw)

    def test_standard_size(self, mocked_randint):
        die = self._make_one()
        result = die.roll()

        mocked_randint.assert_called_with(1, 6)
        self.assertEqual(result, 3)

    def test_custom_size(self, mocked_randint):
        die = self._make_one(sides=42)
        result = die.roll()

        mocked_randint.assert_called_with(1, 42)
        self.assertEqual(result, 3)


if __name__ == '__main__':
    unittest.main()

Với chế độ chế giễu, bài kiểm tra của bạn bây giờ rất đơn giản; chỉ có 2 trường hợp Trường hợp mặc định cho khuôn 6 mặt và trường hợp mặt tùy chỉnh.

Có nhiều cách khác để tạm thời thay thế randint()hàm trong không gian tên toàn cục Die, nhưng mockmô-đun làm cho việc này dễ nhất. Trình @mock.patchtrang trí ở đây áp dụng cho tất cả các phương pháp thử nghiệm trong trường hợp thử nghiệm; mỗi phương thức kiểm tra được thông qua một đối số phụ, random.randint()hàm giả định, vì vậy chúng ta có thể kiểm tra đối với bản giả để xem liệu nó có thực sự được gọi đúng không. Đối return_valuesố chỉ định những gì được trả về từ giả khi nó được gọi, vì vậy chúng tôi có thể xác minh rằng die.roll()phương thức thực sự trả về kết quả 'ngẫu nhiên' cho chúng tôi.

Tôi đã sử dụng một thực hành tốt nhất khác của Python ở đây: nhập lớp đang kiểm tra như một phần của bài kiểm tra. Các _make_onephương pháp thực hiện việc nhập khẩu và instantiation làm việc trong một thử nghiệm , do đó kiểm tra mô-đun vẫn sẽ được tải ngay cả khi bạn đã thực hiện một lỗi cú pháp hoặc sai lầm khác mà sẽ ngăn chặn các mô-đun ban đầu để nhập khẩu.

Bằng cách này, nếu bạn đã mắc lỗi trong chính mã mô-đun, các bài kiểm tra sẽ vẫn được chạy; họ sẽ thất bại, nói với bạn về lỗi trong mã của bạn.

Để rõ ràng, các bài kiểm tra trên là đơn giản trong cực đoan. Mục tiêu ở đây không phải là kiểm tra mà random.randint()đã được gọi với các đối số đúng, chẳng hạn. Thay vào đó, mục tiêu là kiểm tra xem đơn vị đó có tạo ra kết quả đúng với các đầu vào nhất định không, trong đó các đầu vào đó bao gồm kết quả của các đơn vị khác không được thử nghiệm. Bằng cách chế nhạo random.randint()phương thức bạn có thể kiểm soát chỉ một đầu vào khác cho mã của bạn.

Trong các thử nghiệm trong thế giới thực , mã thực tế trong bài kiểm tra đơn vị của bạn sẽ phức tạp hơn; mối quan hệ với các đầu vào được truyền cho API và cách các đơn vị khác được gọi có thể vẫn thú vị và việc chế giễu sẽ cung cấp cho bạn quyền truy cập vào kết quả trung gian, cũng như cho phép bạn đặt giá trị trả về cho các cuộc gọi đó.

Ví dụ: trong mã xác thực người dùng với dịch vụ OAuth2 của bên thứ 3 (tương tác nhiều giai đoạn), bạn muốn kiểm tra xem mã của bạn có truyền đúng dữ liệu cho dịch vụ bên thứ 3 đó không và cho phép bạn giả mạo các phản hồi lỗi khác nhau mà Dịch vụ bên thứ 3 sẽ quay trở lại, cho phép bạn mô phỏng các kịch bản khác nhau mà không phải tự mình xây dựng máy chủ OAuth2. Ở đây, điều quan trọng là phải kiểm tra thông tin từ phản hồi đầu tiên đã được xử lý chính xác và đã được chuyển sang cuộc gọi ở giai đoạn thứ hai, vì vậy bạn muốn thấy rằng dịch vụ giả được gọi là chính xác.


1
Bạn đã có khá nhiều hơn 2 trường hợp thử nghiệm ... kết quả kiểm tra giá trị mặc định: thấp hơn (1), trên (6), dưới thấp hơn (0), ngoài trên (7) và kết quả cho các số do người dùng chỉ định như max_int vv đầu vào cũng không được xác thực, có thể cần phải được kiểm tra tại một số điểm ...
James Snell

2
Không, đó là những bài kiểm tra randint(), không phải mã trong Die.roll().
Martijn Pieters

Thực sự có một cách để đảm bảo rằng không chỉ randint được gọi một cách chính xác mà kết quả của nó cũng được sử dụng đúng: giả sử nó trả về một sentinel.dieví dụ (đối tượng sentinel unittest.mockcũng vậy) và sau đó xác minh rằng đó là những gì được trả về từ phương thức cuộn của bạn. Điều này thực sự chỉ cho phép một cách thực hiện phương pháp được thử nghiệm.
aragaer

@aragaer: chắc chắn, nếu bạn muốn xác minh rằng giá trị được trả về không thay đổi, sentinel.diesẽ là một cách tuyệt vời để đảm bảo điều đó.
Martijn Pieters

Tôi không hiểu tại sao bạn muốn đảm bảo rằng mocked_randint được gọi là với các giá trị nhất định. Tôi hiểu rằng muốn chế nhạo randint để trả về các giá trị có thể dự đoán được, nhưng không phải mối quan tâm chỉ là nó trả về các giá trị có thể dự đoán được chứ không phải nó được gọi là giá trị nào? Dường như với tôi rằng việc kiểm tra các giá trị được gọi là không cần thiết buộc thử nghiệm vào các chi tiết thực hiện tốt. Ngoài ra tại sao chúng ta quan tâm rằng cái chết trả về giá trị chính xác của randint? Chúng ta thực sự không quan tâm rằng nó trả về giá trị> 1 và nhỏ hơn giá trị tối đa?
bdrx

16

Câu trả lời của Martijn là cách bạn thực hiện nếu bạn thực sự muốn chạy thử nghiệm chứng minh rằng bạn đang gọi ngẫu nhiên.randint. Tuy nhiên, có nguy cơ bị nói "không trả lời câu hỏi", tôi cảm thấy điều này không nên được kiểm tra đơn vị. Mocking randint không còn là thử nghiệm hộp đen - bạn đặc biệt cho thấy rằng một số điều đang diễn ra trong quá trình thực hiện . Kiểm tra hộp đen thậm chí không phải là một tùy chọn - không có thử nghiệm nào bạn có thể thực hiện sẽ chứng minh rằng kết quả sẽ không bao giờ nhỏ hơn 1 hoặc nhiều hơn 6.

Bạn có thể chế giễu randint? Vâng, bạn có thể. Nhưng bạn đang chứng minh điều gì? Rằng bạn gọi nó với các đối số 1 và các mặt. Điều đó có nghĩa là gì? Bạn quay lại quảng trường một - vào cuối ngày, bạn sẽ phải chứng minh - chính thức hoặc không chính thức - rằng việc gọi random.randint(1, sides)chính xác thực hiện một cuộn súc sắc.

Tôi là tất cả để thử nghiệm đơn vị. Họ kiểm tra sự tỉnh táo tuyệt vời và phơi bày sự hiện diện của bọ. Tuy nhiên, họ không bao giờ có thể chứng minh sự vắng mặt của mình và có những điều không thể được khẳng định thông qua thử nghiệm (ví dụ: một chức năng cụ thể không bao giờ ném ngoại lệ hoặc luôn chấm dứt.) Trong trường hợp cụ thể này, tôi cảm thấy có rất ít bạn đứng trước thu được. Đối với hành vi mang tính quyết định, các bài kiểm tra đơn vị có ý nghĩa bởi vì bạn thực sự biết câu trả lời mà bạn mong đợi sẽ là gì.


Kiểm tra đơn vị không phải là kiểm tra hộp đen, thực sự. Đó là những gì kiểm tra tích hợp dành cho, để thấy rằng các phần khác nhau tương tác như được thiết kế. Tất nhiên, đó là vấn đề về quan điểm (hầu hết triết lý kiểm tra là), xem "Thử nghiệm đơn vị" có nằm trong thử nghiệm hộp trắng hoặc hộp đen không? Kiểm thử đơn vị hộp đen cho một số quan điểm (Stack Overflow).
Martijn Pieters

@MartijnPieters Tôi không đồng ý rằng "đó là những gì kiểm tra tích hợp dành cho". Kiểm tra tích hợp là để kiểm tra xem tất cả các thành phần của hệ thống tương tác chính xác. Chúng không phải là nơi để kiểm tra rằng một thành phần nhất định cho đầu ra chính xác cho đầu vào đã cho. Đối với thử nghiệm hộp đen so với thử nghiệm đơn vị hộp trắng, các thử nghiệm đơn vị hộp trắng cuối cùng sẽ phá vỡ với các thay đổi triển khai và mọi giả định bạn đã thực hiện trong quá trình triển khai có thể sẽ được đưa vào thử nghiệm. Xác nhận điều đó random.randintđược gọi 1, sideslà vô ích nếu đó là điều sai trái.
Doval

Vâng, đó là một hạn chế của bài kiểm tra đơn vị hộp trắng. Tuy nhiên, không có điểm nào trong thử nghiệm random.randint()sẽ trả về chính xác các giá trị trong phạm vi [1, các mặt] (bao gồm), điều đó tùy thuộc vào các nhà phát triển Python để đảm bảo rằng randomthiết bị hoạt động chính xác.
Martijn Pieters

Và như bạn tự nói, kiểm thử đơn vị không thể đảm bảo rằng mã của bạn không có lỗi; nếu mã của bạn sử dụng sai các đơn vị khác (giả sử, bạn dự kiến ​​sẽ random.randint()hành xử như random.randrange()vậy và do đó gọi nó với random.randint(1, sides + 1), thì dù sao bạn cũng bị chìm.
Martijn Pieters

2
@MartijnPieters Tôi đồng ý với bạn ở đó, nhưng đó không phải là điều tôi phản đối. Tôi phản đối việc kiểm tra Random.randint đang được gọi với các đối số (1, các mặt) . Bạn đã giả định trong việc thực hiện rằng đây là điều chính xác phải làm, và bây giờ bạn đang lặp lại giả định đó trong bài kiểm tra. Nếu giả định đó là sai, bài kiểm tra sẽ vượt qua nhưng việc thực hiện của bạn vẫn không đúng. Đó là một bằng chứng nửa khẳng định đó là một sự đau đớn hoàn toàn để viết và duy trì.
Doval

6

Sửa hạt giống ngẫu nhiên. Đối với xúc xắc 1, 2, 5 và 12 mặt, hãy xác nhận rằng một vài nghìn cuộn cho kết quả bao gồm 1 và N, và không bao gồm 0 hoặc N + 1. Nếu có vẻ kỳ quặc, bạn sẽ nhận được một tập kết quả ngẫu nhiên mà không bao gồm phạm vi dự kiến, chuyển sang một hạt giống khác.

Công cụ nhạo báng rất tuyệt, nhưng chỉ vì chúng cho phép bạn làm một việc không có nghĩa là việc đó nên được thực hiện. YAGNI áp dụng cho đồ đạc thử nghiệm nhiều như các tính năng.

Nếu bạn có thể dễ dàng kiểm tra với các phụ thuộc không bị khóa, bạn luôn luôn nên; bằng cách đó, các bài kiểm tra của bạn sẽ được tập trung vào việc giảm số lượng khuyết tật, không chỉ tăng số lượng kiểm tra. Rủi ro chế giễu quá mức tạo ra các số liệu bảo hiểm sai lệch, do đó có thể dẫn đến hoãn thử nghiệm thực tế đến một số giai đoạn sau mà bạn có thể không bao giờ có thời gian để làm tròn ...


3

A là gì Dienếu bạn nghĩ về nó? - không nhiều hơn một bọc xung quanh random. Nó gói gọn random.randintvà liên kết nó theo từ vựng riêng của ứng dụng của bạn : Die.Roll.

Tôi không thấy có liên quan để chèn một lớp trừu tượng khác giữa Dierandombởi vì Diebản thân nó đã là lớp không xác định giữa ứng dụng của bạn và nền tảng.

Nếu bạn muốn kết quả xúc xắc đóng hộp, chỉ cần chế giễu Die, đừng chế giễurandom .

Nói chung, tôi không kiểm tra đơn vị các đối tượng trình bao bọc của mình giao tiếp với các hệ thống bên ngoài, tôi viết các bài kiểm tra tích hợp cho chúng. Bạn có thể viết một vài trong số đó cho Dienhưng như bạn đã chỉ ra, do tính chất ngẫu nhiên của đối tượng cơ bản, chúng sẽ không có ý nghĩa. Ngoài ra, không có cấu hình hoặc giao tiếp mạng liên quan ở đây nên không có nhiều thử nghiệm ngoại trừ một cuộc gọi nền tảng.

=> Xem xét rằng đó Diechỉ là một vài dòng mã tầm thường và thêm ít hoặc không có logic so với randomchính nó, tôi bỏ qua việc kiểm tra nó trong ví dụ cụ thể đó.


2

Tạo hạt giống cho trình tạo số ngẫu nhiên và xác minh kết quả dự kiến ​​là KHÔNG, theo như tôi có thể thấy, một thử nghiệm hợp lệ. Nó đưa ra các giả định về cách thức súc sắc của bạn hoạt động bên trong, đó là nghịch ngợm. Các nhà phát triển của python có thể thay đổi trình tạo số ngẫu nhiên hoặc die (LƯU Ý: "xúc xắc" là số nhiều, "die" là số ít. Trừ khi lớp của bạn thực hiện nhiều cuộn chết trong một cuộc gọi, có lẽ nên gọi là "die") sử dụng một bộ tạo số ngẫu nhiên khác nhau.

Tương tự, chế độ hàm ngẫu nhiên giả định rằng việc thực hiện lớp hoạt động chính xác như mong đợi. Tại sao điều này có thể không phải là trường hợp? Ai đó có thể kiểm soát trình tạo số ngẫu nhiên python mặc định và để tránh điều đó, một phiên bản tương lai của cái chết của bạn có thể lấy một số số ngẫu nhiên hoặc số ngẫu nhiên lớn hơn để trộn vào dữ liệu ngẫu nhiên hơn. Một sơ đồ tương tự đã được sử dụng bởi các nhà sản xuất hệ điều hành FreeBSD, khi họ nghi ngờ NSA đang giả mạo các bộ tạo số ngẫu nhiên phần cứng được tích hợp trong CPU.

Nếu là tôi, tôi sẽ chạy, giả sử, 6000 cuộn, kiểm đếm chúng và đảm bảo rằng mỗi số từ 1-6 được cuộn từ 500 đến 1500 lần. Tôi cũng sẽ kiểm tra xem không có số nào nằm ngoài phạm vi đó được trả về. Tôi cũng có thể kiểm tra xem, đối với bộ 6000 cuộn thứ hai, khi sắp xếp [1..6] theo thứ tự tần số, kết quả sẽ khác (điều này sẽ thất bại một lần trong số 720 lần chạy, nếu các số là ngẫu nhiên!). Nếu bạn muốn kỹ lưỡng, bạn có thể tìm thấy tần suất của các số theo sau 1, sau 2, v.v; nhưng hãy chắc chắn rằng cỡ mẫu của bạn đủ lớn và bạn có đủ phương sai. Con người mong đợi số ngẫu nhiên có ít mẫu hơn so với thực tế.

Lặp lại cho một khuôn mặt 12 mặt và 2 mặt (6 là được sử dụng nhiều nhất, do đó, được mong đợi nhất cho bất kỳ ai viết mã này).

Cuối cùng, tôi sẽ kiểm tra xem điều gì xảy ra với một cái chết 1 mặt, một cái chết 0 mặt, một cái chết 1 mặt, một cái chết 2,3 mặt, một cái chết [1,2,3,4,5,6] và một cái chết "blah" Tất nhiên, những điều này nên thất bại; họ thất bại một cách hữu ích? Đây có lẽ nên thất bại về sáng tạo, không phải trên cán.

Hoặc, có lẽ, bạn cũng muốn xử lý những điều này một cách khác biệt - có lẽ việc tạo ra một cái chết với [1,2,3,4,5,6] nên được chấp nhận - và có lẽ cũng "blah"; đây có thể là một cái chết với 4 khuôn mặt và mỗi khuôn mặt có một chữ cái trên đó. Trò chơi "Boggle" nảy ra trong đầu, cũng như một quả bóng ma thuật tám.

Và cuối cùng, bạn có thể muốn chiêm ngưỡng điều này: http://lh6.ggpht.com/-fAGXwbJbYRM/UJA_31ACOLI/AAAAAAAAAPg/2FxOWzo96KE/s1600-h/random%25255B3%25255.jpg


2

Có nguy cơ bơi ngược dòng thủy triều, tôi đã giải quyết vấn đề chính xác này một số năm trước bằng một phương pháp chưa được đề cập cho đến nay.

Chiến lược của tôi chỉ đơn giản là chế nhạo RNG bằng một chiến lược tạo ra một luồng giá trị có thể dự đoán được bao trùm toàn bộ không gian. Nếu (nói) side = 6 và RNG tạo ra các giá trị từ 0 đến 5 theo thứ tự tôi có thể dự đoán cách lớp của tôi nên hành xử và kiểm tra đơn vị phù hợp.

Lý do là việc này chỉ kiểm tra logic trong lớp này, với giả định rằng RNG cuối cùng sẽ tạo ra từng giá trị đó và không kiểm tra chính RNG.

Nó đơn giản, mang tính quyết định, có thể tái tạo và nó không bắt được lỗi. Tôi sẽ sử dụng chiến lược tương tự một lần nữa.


Câu hỏi không đánh vần những bài kiểm tra nên là gì, chỉ những dữ liệu nào có thể được sử dụng để kiểm tra, với sự có mặt của RNG. Đề nghị của tôi chỉ đơn thuần là kiểm tra toàn diện bằng cách chế nhạo RNG. Câu hỏi về những gì đáng thử nghiệm phụ thuộc vào thông tin không được cung cấp trong câu hỏi.


Nói rằng bạn chế nhạo RNG để có thể dự đoán được. Vậy thì bạn sẽ kiểm tra cái gì? Câu hỏi đặt ra "Liệu các bài kiểm tra đơn vị sau đây có hợp lệ / hữu ích không?" Mocking nó để trả về 0-5 không phải là một thử nghiệm mà là thiết lập thử nghiệm. Làm thế nào bạn sẽ "kiểm tra đơn vị phù hợp"? Tôi không hiểu làm thế nào nó "bắt lỗi". Tôi đang gặp khó khăn trong việc hiểu những gì tôi cần phải kiểm tra 'đơn vị'.
bdrx

@bdrx: Đây là một thời gian trước đây: Tôi sẽ trả lời nó khác bây giờ. Nhưng xem chỉnh sửa.
david.pfx

1

Các bài kiểm tra mà bạn đề xuất trong câu hỏi của bạn không phát hiện bộ đếm số học mô-đun khi triển khai. Và họ không phát hiện ra các lỗi triển khai phổ biến trong mã liên quan đến phân phối xác suất như thế nào return 1 + (random.randint(1,maxint) % sides). Hoặc thay đổi trình tạo dẫn đến các mẫu 2 chiều.

Nếu bạn thực sự muốn xác minh rằng bạn đang tạo các số xuất hiện ngẫu nhiên phân bố đồng đều, bạn cần kiểm tra rất nhiều thuộc tính. Để thực hiện một công việc hợp lý tốt ở đó, bạn có thể chạy http://www.phy.duke.edu/~rgb/General/dieharder.php trên các số được tạo của bạn. Hoặc viết một bộ kiểm tra đơn vị phức tạp tương tự.

Đó không phải là lỗi của kiểm tra đơn vị hoặc TDD, tính ngẫu nhiên chỉ là một thuộc tính rất khó kiểm chứng. Và một chủ đề phổ biến cho các ví dụ.


-1

Thử nghiệm đơn giản nhất của một cuộn súc sắc chỉ đơn giản là lặp lại nó vài trăm nghìn lần và xác nhận rằng mỗi kết quả có thể đã đạt được khoảng (1 / số cạnh) lần. Trong trường hợp chết 6 mặt, bạn sẽ thấy mỗi giá trị có thể đạt khoảng 16,6% thời gian. Nếu bất kỳ được giảm hơn một phần trăm, thì bạn có một vấn đề.

Làm theo cách này để tránh cho phép bạn cấu trúc lại cơ chế cơ bản của việc tạo một số ngẫu nhiên một cách dễ dàng và quan trọng nhất là không thay đổi thử nghiệm.


1
kiểm tra này sẽ vượt qua để thực hiện hoàn toàn không ngẫu nhiên mà chỉ đơn giản là lặp qua các bên theo thứ tự được xác định trước
gnat

1
Nếu một lập trình viên có ý định thực hiện điều gì đó với mục đích xấu (không sử dụng tác nhân ngẫu nhiên khi chết) và chỉ cần cố gắng tìm thứ gì đó để 'làm cho đèn đỏ chuyển sang màu xanh' thì bạn có thể giải quyết được nhiều vấn đề hơn là thử nghiệm đơn vị thực sự có thể giải quyết.
ChristopherBrown
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.