Thiết kế kiểm tra đơn vị cho một hệ thống nhà nước


20

Lý lịch

Test Driven Development đã được phổ biến sau khi tôi học xong và trong ngành. Tôi đang cố gắng học nó, nhưng một số điều quan trọng vẫn thoát khỏi tôi. Những người đề xuất TDD nói rất nhiều điều như (sau đây gọi là "nguyên tắc khẳng định duy nhất" hoặc SAP ):

Đôi khi tôi đã suy nghĩ về cách các bài kiểm tra TDD có thể đơn giản, biểu cảm và thanh lịch nhất có thể. Bài viết này tìm hiểu một chút về những gì nó muốn làm cho các bài kiểm tra đơn giản và phân tách nhất có thể: nhắm đến một khẳng định duy nhất trong mỗi bài kiểm tra.

Nguồn: http://www.artima.com/weblogs/viewpost.jsp?thread=35578

Họ cũng nói những điều như thế này (sau đây gọi là "nguyên tắc phương pháp riêng" hoặc PMP ):

Bạn thường không thử nghiệm phương pháp riêng tư trực tiếp. Vì chúng là riêng tư, hãy xem chúng là một chi tiết thực hiện. Không ai sẽ gọi một trong số họ và mong đợi nó hoạt động theo một cách riêng.

Thay vào đó bạn nên kiểm tra giao diện công cộng của bạn. Nếu các phương thức gọi các phương thức riêng tư của bạn đang hoạt động như bạn mong đợi, thì bạn giả sử bằng cách mở rộng rằng các phương thức riêng tư của bạn đang hoạt động chính xác.

Nguồn: Làm thế nào để bạn đơn vị thử nghiệm phương pháp riêng tư?

Tình hình

Tôi đang cố gắng để kiểm tra một hệ thống xử lý dữ liệu trạng thái. Hệ thống có thể thực hiện những việc khác nhau cho cùng một phần dữ liệu dựa trên trạng thái trước khi nhận dữ liệu đó. Xem xét một thử nghiệm đơn giản, xây dựng trạng thái trong hệ thống, sau đó kiểm tra hành vi mà phương thức đã cho là nhằm kiểm tra.

  • SAP đề nghị rằng tôi không nên thử nghiệm "quy trình xây dựng trạng thái", tôi nên cho rằng trạng thái đó là những gì tôi mong đợi từ mã xây dựng và sau đó kiểm tra thay đổi trạng thái mà tôi đang thử nghiệm

  • PMP gợi ý rằng tôi không thể bỏ qua bước "xây dựng trạng thái" này và chỉ kiểm tra các phương pháp chi phối chức năng đó một cách độc lập.

Kết quả trong mã thực tế của tôi là các bài kiểm tra cồng kềnh, phức tạp, dài và khó viết. Và nếu sự chuyển đổi trạng thái thay đổi, các thử nghiệm phải được thay đổi ... sẽ ổn với các thử nghiệm nhỏ, hiệu quả nhưng cực kỳ tốn thời gian và khó hiểu với các thử nghiệm cồng kềnh này. Làm thế nào là điều này thường được thực hiện?


2
Tôi không nghĩ bạn sẽ tìm thấy một giải pháp tao nhã cho vấn đề này. Cách tiếp cận chung là không làm cho hệ thống trở nên bắt đầu, điều này không giúp ích gì cho bạn khi thử nghiệm thứ gì đó đã được xây dựng. Tái cấu trúc nó thành không trạng thái có lẽ cũng không đáng giá.
Doval


@Doval: Vui lòng giải thích cách làm cho một cái gì đó giống như điện thoại (SIP UserAgent) không trạng thái. Hành vi dự kiến ​​của đơn vị này được chỉ định trong RFC bằng sơ đồ chuyển trạng thái.
Bart van Ingen Schenau

Bạn đang sao chép / dán / chỉnh sửa các bài kiểm tra của mình hay bạn đang viết các phương thức tiện ích để chia sẻ thiết lập / phân tích / chức năng chung? Mặc dù một số trường hợp thử nghiệm chắc chắn có thể kéo dài và đầy hơi, nhưng điều này không nên phổ biến. Trong một hệ thống trạng thái, tôi sẽ mong đợi một thói quen thiết lập chung trong đó trạng thái kết thúc là một tham số và thói quen này đưa bạn đến trạng thái bạn muốn kiểm tra. Ngoài ra, vào cuối mỗi bài kiểm tra, tôi sẽ có một phương pháp phân tích để đưa bạn trở lại trạng thái bắt đầu đã biết (nếu điều đó là bắt buộc) để phương thức thiết lập của bạn sẽ hoạt động bình thường khi bài kiểm tra tiếp theo bắt đầu.
Dunk

Trên một tiếp tuyến nhưng tôi cũng sẽ thêm rằng các sơ đồ trạng thái là một công cụ giao tiếp và không phải là một nghị định triển khai ngay cả khi nó nằm trong RFC. Miễn là bạn đáp ứng các chức năng được mô tả là bạn đáp ứng tiêu chuẩn. Tôi đã có một vài lần tôi chuyển đổi các triển khai chuyển đổi trạng thái thực sự phức tạp (như được định nghĩa trong RFC) thành chức năng xử lý chung thực sự đơn giản. Một trường hợp tôi nhớ đã loại bỏ vài nghìn dòng mã khi tôi nhận ra rằng ngoài một vài lá cờ, khoảng 5 trạng thái đã làm chính xác điều tương tự khi bạn đổi tên các thành phần chung "ẩn".
Dunk

Câu trả lời:


15

Phối cảnh:

Vì vậy, hãy lùi lại một bước và hỏi TDD đang cố gắng giúp chúng tôi điều gì. TDD đang cố gắng giúp chúng tôi xác định xem mã của chúng tôi có đúng hay không. Và theo đúng, tôi có nghĩa là "mã có đáp ứng các yêu cầu kinh doanh không?" Điểm bán hàng là chúng tôi biết những thay đổi sẽ được yêu cầu trong tương lai và chúng tôi muốn đảm bảo rằng mã của chúng tôi vẫn chính xác sau khi chúng tôi thực hiện những thay đổi đó.

Tôi đưa quan điểm đó lên bởi vì tôi nghĩ rằng thật dễ dàng để bị lạc trong các chi tiết và đánh mất những gì chúng ta đang cố gắng đạt được.

Nguyên tắc - SAP:

Mặc dù tôi không phải là chuyên gia về TDD, tôi nghĩ rằng bạn đang thiếu một phần của Nguyên tắc Xác nhận Đơn lẻ (SAP) đang cố gắng dạy. SAP có thể được trình bày lại dưới dạng "kiểm tra từng thứ một." Nhưng TOTAT không thể tặc lưỡi dễ dàng như SAP.

Kiểm tra một điều tại một thời điểm có nghĩa là bạn tập trung vào một trường hợp; một con đường; một điều kiện biên; một trường hợp lỗi; một bất cứ điều gì cho mỗi bài kiểm tra. Và ý tưởng lái xe đằng sau đó là bạn cần biết những gì đã phá vỡ khi trường hợp thử nghiệm thất bại, để bạn có thể giải quyết vấn đề nhanh hơn. Nếu bạn kiểm tra nhiều điều kiện (nghĩa là nhiều hơn một điều) trong một bài kiểm tra và bài kiểm tra thất bại, thì bạn sẽ có nhiều công việc hơn trong tay. Trước tiên, bạn phải xác định trường hợp nào trong số nhiều trường hợp thất bại và sau đó tìm hiểu tại sao trường hợp đó thất bại.

Nếu bạn kiểm tra từng thứ một, phạm vi tìm kiếm của bạn nhỏ hơn rất nhiều và lỗi được xác định nhanh hơn. Hãy ghi nhớ rằng "kiểm tra từng thứ một" không nhất thiết loại trừ bạn khỏi việc xem xét nhiều hơn một đầu ra quy trình cùng một lúc. Ví dụ: khi kiểm tra "đường dẫn tốt đã biết", tôi có thể mong đợi thấy một giá trị cụ thể, kết quả foocũng như một giá trị khác barvà tôi có thể xác minh đó foo != barlà một phần của thử nghiệm của mình. Điều quan trọng là nhóm hợp lý các kiểm tra đầu ra dựa trên trường hợp được kiểm tra.

Nguyên tắc - PMP:

Tương tự như vậy, tôi nghĩ rằng bạn đang thiếu một chút về Nguyên tắc Phương pháp Riêng tư (PMP) phải dạy chúng ta điều gì. PMP khuyến khích chúng ta coi hệ thống như một hộp đen. Đối với một đầu vào nhất định, bạn sẽ nhận được một đầu ra nhất định. Bạn không quan tâm làm thế nào hộp đen tạo ra đầu ra. Bạn chỉ quan tâm rằng đầu ra của bạn phù hợp với đầu vào của bạn.

PMP thực sự là viễn cảnh tốt để xem xét các khía cạnh API của mã của bạn. Nó cũng có thể giúp bạn phạm vi những gì bạn phải kiểm tra. Xác định các điểm giao diện của bạn và xác minh rằng họ đáp ứng các điều khoản trong hợp đồng của họ. Bạn không cần quan tâm đến việc các phương thức phía sau giao diện (còn gọi là riêng tư) thực hiện công việc của họ như thế nào. Bạn chỉ cần xác minh họ đã làm những gì họ phải làm.


Áp dụng TDD ( cho bạn )

Vì vậy, tình huống của bạn thể hiện một chút nếp nhăn ngoài một ứng dụng thông thường. Các phương thức của ứng dụng của bạn là trạng thái, do đó, đầu ra của chúng phụ thuộc vào không chỉ đầu vào mà cả những gì đã được thực hiện trước đó. Tôi chắc rằng tôi nên <insert some lecture>ở đây về tình trạng khủng khiếp và blah blah blah, nhưng điều đó thực sự không giúp giải quyết vấn đề của bạn.

Tôi sẽ giả định rằng bạn có một số loại biểu đồ trạng thái cho thấy các trạng thái tiềm năng khác nhau và những gì cần phải được thực hiện để kích hoạt chuyển đổi. Nếu bạn không, bạn sẽ cần nó vì nó sẽ giúp thể hiện các yêu cầu kinh doanh cho hệ thống này.

Các bài kiểm tra: Đầu tiên, bạn sẽ kết thúc với một loạt các bài kiểm tra ban hành thay đổi trạng thái. Lý tưởng nhất là bạn sẽ có các bài kiểm tra thực hiện đầy đủ các thay đổi trạng thái có thể xảy ra nhưng tôi có thể thấy một vài tình huống mà bạn có thể không cần phải đi hết mức đó.

Tiếp theo, bạn cần xây dựng các bài kiểm tra để xác nhận việc xử lý dữ liệu. Một số bài kiểm tra trạng thái sẽ được sử dụng lại khi bạn tạo các bài kiểm tra xử lý dữ liệu. Ví dụ: giả sử bạn có một phương thức Foo()có các đầu ra khác nhau dựa trên một InitState1trạng thái. Bạn sẽ muốn sử dụng ChangeFooToState1thử nghiệm của mình làm bước thiết lập để kiểm tra đầu ra khi " Foo()ở trong State1".

Có một số hàm ý đằng sau cách tiếp cận mà tôi muốn đề cập. Spoiler, đây là nơi tôi sẽ chọc tức những người theo chủ nghĩa thuần túy

Trước hết, bạn phải chấp nhận rằng bạn sử dụng một cái gì đó để thử nghiệm trong một tình huống và thiết lập trong một tình huống khác. Một mặt, điều này dường như là vi phạm trực tiếp của SAP. Nhưng nếu bạn hợp lý ChangeFooToState1có hai mục đích thì bạn vẫn đáp ứng được tinh thần của những gì SAP đang dạy chúng ta. Khi bạn cần đảm bảo Foo()thay đổi trạng thái, sau đó bạn sử dụng ChangeFooToState1làm bài kiểm tra. Và khi cần xác thực Foo()đầu ra của "khi vào State1" thì bạn đang sử dụng ChangeFooToState1làm thiết lập.

Mục thứ hai là từ quan điểm thực tế, bạn sẽ không muốn thử nghiệm đơn vị hoàn toàn ngẫu nhiên cho hệ thống của bạn. Bạn nên chạy tất cả các bài kiểm tra thay đổi trạng thái trước khi chạy các bài kiểm tra xác thực đầu ra. SAP là loại nguyên tắc chỉ đạo đằng sau việc đặt hàng đó. Để nói rõ điều gì là hiển nhiên - bạn không thể sử dụng một cái gì đó như thiết lập nếu nó thất bại như một thử nghiệm.

Đặt nó lại với nhau:

Sử dụng sơ đồ trạng thái của bạn, bạn sẽ tạo các bài kiểm tra để bao quát các chuyển đổi. Một lần nữa, bằng cách sử dụng sơ đồ của bạn, bạn tạo các thử nghiệm để bao gồm tất cả các trường hợp xử lý dữ liệu đầu vào / đầu ra được điều khiển bởi trạng thái.

Nếu bạn làm theo cách tiếp cận đó, các bloated, complicated, long, and difficult to writebài kiểm tra sẽ dễ quản lý hơn một chút. Nói chung, chúng nên kết thúc nhỏ hơn và chúng nên ngắn gọn hơn (nghĩa là ít phức tạp hơn). Bạn nên chú ý rằng các bài kiểm tra cũng được tách rời hoặc mô-đun nhiều hơn.

Bây giờ, tôi không nói rằng quá trình này sẽ hoàn toàn không đau đớn bởi vì viết các bài kiểm tra tốt sẽ mất một số nỗ lực. Và một số trong số chúng vẫn sẽ khó khăn vì bạn đang ánh xạ một tham số thứ hai (trạng thái) qua khá nhiều trường hợp của bạn. Và như một bên, nó nên rõ ràng hơn một chút tại sao một hệ thống không trạng thái là dễ dàng hơn để xây dựng các bài kiểm tra. Nhưng nếu bạn điều chỉnh cách tiếp cận này cho ứng dụng của mình, bạn sẽ thấy rằng bạn có thể chứng minh rằng ứng dụng của bạn đang hoạt động chính xác.


11

Bạn thường sẽ trừu tượng hóa các chi tiết thiết lập thành các chức năng để bạn không phải lặp lại chính mình. Bằng cách đó, bạn chỉ phải thay đổi nó ở một nơi trong thử nghiệm nếu chức năng thay đổi.

Tuy nhiên, thông thường bạn sẽ không muốn mô tả ngay cả các chức năng thiết lập của mình là cồng kềnh, phức tạp hoặc dài. Đó là một dấu hiệu cho thấy giao diện của bạn cần tái cấu trúc, bởi vì nếu việc kiểm tra của bạn khó sử dụng, thì mã thực của bạn cũng khó sử dụng.

Đó thường là một dấu hiệu của việc đưa quá nhiều vào một lớp. Nếu bạn có yêu cầu về trạng thái, bạn cần một lớp quản lý trạng thái và không có gì khác. Các lớp hỗ trợ nó nên không trạng thái. Đối với ví dụ SIP của bạn, phân tích cú pháp một gói nên hoàn toàn không trạng thái. Bạn có thể có một lớp phân tích cú pháp một gói sau đó gọi một cái gì đó như sipStateController.receiveInvite()để quản lý các chuyển đổi trạng thái, chính nó gọi các lớp không trạng thái khác để làm những việc như đổ chuông điện thoại.

Điều này làm cho việc thiết lập thử nghiệm đơn vị cho lớp máy trạng thái là vấn đề đơn giản của một vài cuộc gọi phương thức. Nếu thiết lập của bạn cho các bài kiểm tra đơn vị máy trạng thái yêu cầu các gói chế tạo, bạn đã đặt quá nhiều vào lớp đó. Tương tự, lớp trình phân tích cú pháp gói của bạn phải tương đối đơn giản để tạo mã thiết lập cho, sử dụng một giả cho lớp máy trạng thái.

Nói cách khác, bạn không thể tránh trạng thái hoàn toàn, nhưng bạn có thể giảm thiểu và cô lập nó.


Chỉ để ghi lại, ví dụ SIP là của tôi, không phải từ OP. Và một số máy trạng thái có thể cần nhiều hơn một vài lệnh gọi phương thức để đưa chúng vào trạng thái phù hợp cho một thử nghiệm nhất định.
Bart van Ingen Schenau

+1 cho "bạn không thể tránh trạng thái hoàn toàn, nhưng bạn có thể giảm thiểu và cô lập nó." Tôi không thể đồng ý. Nhà nước là một cái ác cần thiết trong phần mềm.
Brandon

0

Ý tưởng cốt lõi của TDD là, bằng cách viết các bài kiểm tra trước, bạn kết thúc với một hệ thống, ít nhất, dễ kiểm tra. Hy vọng rằng nó hoạt động, có thể duy trì, tài liệu tốt và như vậy, nhưng nếu không, ít nhất nó vẫn dễ dàng để kiểm tra.

Vì vậy, nếu bạn TDD và kết thúc với một hệ thống khó kiểm tra, đã xảy ra lỗi. Có lẽ một số điều là riêng tư nên được công khai, bởi vì bạn cần chúng để thử nghiệm. Có lẽ bạn không làm việc ở mức độ trừu tượng phù hợp; một cái gì đó đơn giản như một danh sách là trạng thái ở một cấp độ, nhưng một giá trị ở cấp độ khác. Hoặc có lẽ bạn đang đưa ra quá nhiều trọng lượng cho lời khuyên không thể áp dụng trong ngữ cảnh của bạn, hoặc vấn đề của bạn chỉ là khó khăn. Hoặc, tất nhiên, có lẽ thiết kế của bạn chỉ là xấu.

Dù nguyên nhân là gì, có lẽ bạn sẽ không quay lại và viết lại hệ thống của mình để làm cho nó dễ kiểm tra hơn với mã kiểm tra đơn giản. Vì vậy, có khả năng kế hoạch tốt nhất là sử dụng một số kỹ thuật kiểm tra lạ mắt hơn một chút, như:

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.