Tôi không thích thử nghiệm chức năng riêng tư vì một vài lý do. Chúng là như sau (đây là những điểm chính cho người TLDR):
- Thông thường khi bạn muốn thử nghiệm phương thức riêng tư của lớp, đó là mùi thiết kế.
- Bạn có thể kiểm tra chúng thông qua giao diện công cộng (đó là cách bạn muốn kiểm tra chúng, bởi vì đó là cách khách hàng sẽ gọi / sử dụng chúng). Bạn có thể có được cảm giác an toàn sai lầm bằng cách nhìn thấy đèn xanh trên tất cả các bài kiểm tra vượt qua cho các phương pháp riêng tư của bạn. Sẽ tốt hơn / an toàn hơn khi kiểm tra các trường hợp cạnh trên các chức năng riêng tư của bạn thông qua giao diện chung.
- Bạn có nguy cơ trùng lặp kiểm tra nghiêm trọng (các xét nghiệm trông / cảm thấy rất giống nhau) bằng cách thử nghiệm các phương pháp riêng tư. Điều này có hậu quả lớn khi các yêu cầu thay đổi, vì nhiều thử nghiệm hơn mức cần thiết sẽ bị phá vỡ. Nó cũng có thể đặt bạn vào một vị trí khó tái cấu trúc vì bộ thử nghiệm của bạn ... đó là điều trớ trêu cuối cùng, bởi vì bộ thử nghiệm có mặt để giúp bạn thiết kế lại và tái cấu trúc một cách an toàn!
Tôi sẽ giải thích từng điều này bằng một ví dụ cụ thể. Hóa ra 2) và 3) có mối liên hệ khá phức tạp, vì vậy ví dụ của chúng tương tự nhau, mặc dù tôi coi chúng là những lý do riêng biệt tại sao bạn không nên thử nghiệm các phương pháp riêng tư.
Đôi khi thử nghiệm các phương pháp riêng tư là phù hợp, điều quan trọng là phải nhận thức được những nhược điểm được liệt kê ở trên. Tôi sẽ đi qua chi tiết hơn sau.
Tôi cũng đi qua lý do tại sao TDD không phải là một lý do hợp lệ để thử nghiệm các phương thức riêng tư vào cuối.
Tái cấu trúc theo cách của bạn ra khỏi một thiết kế xấu
Một trong những trò chơi phổ biến (chống) mà tôi thấy là những gì Michael Feathers gọi là lớp "Iceberg" (nếu bạn không biết Michael Feathers là ai, hãy mua / đọc cuốn sách "Làm việc hiệu quả với Bộ luật kế thừa". một người đáng để biết nếu bạn là một kỹ sư / nhà phát triển phần mềm chuyên nghiệp). Có những mô hình (chống) khác khiến vấn đề này tăng lên, nhưng đây là mô hình phổ biến nhất mà tôi đã vấp phải. Các lớp "Iceberg" có một phương thức chung và phần còn lại là riêng tư (đó là lý do tại sao nó muốn thử nghiệm các phương thức riêng tư). Nó được gọi là lớp "Iceberg" bởi vì thường có một phương thức công khai đơn độc, nhưng phần còn lại của chức năng được ẩn dưới nước dưới dạng phương thức riêng.
Ví dụ, bạn có thể muốn kiểm tra GetNextToken()
bằng cách gọi nó trên một chuỗi liên tiếp và thấy rằng nó trả về kết quả mong đợi. Một chức năng như thế này đảm bảo kiểm tra: hành vi đó không tầm thường, đặc biệt nếu quy tắc mã thông báo của bạn phức tạp. Hãy giả vờ rằng nó không quá phức tạp và chúng tôi chỉ muốn kết nối các mã thông báo được phân định bởi không gian. Vì vậy, bạn viết một bài kiểm tra, có thể nó trông giống như thế này (một số mã psuedo bất khả tri ngôn ngữ, hy vọng ý tưởng này rõ ràng):
TEST_THAT(RuleEvaluator, canParseSpaceDelimtedTokens)
{
input_string = "1 2 test bar"
re = RuleEvaluator(input_string);
ASSERT re.GetNextToken() IS "1";
ASSERT re.GetNextToken() IS "2";
ASSERT re.GetNextToken() IS "test";
ASSERT re.GetNextToken() IS "bar";
ASSERT re.HasMoreTokens() IS FALSE;
}
Chà, điều đó thực sự trông khá đẹp. Chúng tôi muốn đảm bảo rằng chúng tôi duy trì hành vi này khi chúng tôi thực hiện các thay đổi. Nhưng GetNextToken()
là một chức năng riêng tư ! Vì vậy, chúng tôi không thể kiểm tra nó như thế này, vì nó thậm chí không biên dịch (giả sử chúng tôi đang sử dụng một số ngôn ngữ thực sự thực thi công khai / riêng tư, không giống như một số ngôn ngữ kịch bản như Python). Nhưng còn việc thay đổi RuleEvaluator
lớp học để tuân theo Nguyên tắc Trách nhiệm Đơn lẻ (Nguyên tắc Trách nhiệm Đơn lẻ) thì sao? Chẳng hạn, chúng ta dường như có một trình phân tích cú pháp, mã thông báo và trình đánh giá bị kẹt vào một lớp. Sẽ không tốt hơn nếu chỉ tách những trách nhiệm đó? Ngày đầu đó, nếu bạn tạo một Tokenizer
lớp, sau đó phương pháp nào nó sẽ là HasMoreTokens()
và GetNextTokens()
. Các RuleEvaluator
lớp có thể có mộtTokenizer
đối tượng như một thành viên. Bây giờ, chúng ta có thể giữ bài kiểm tra tương tự như trên, ngoại trừ chúng ta đang kiểm tra Tokenizer
lớp thay vì RuleEvaluator
lớp.
Đây là những gì nó có thể trông giống như trong UML:
Lưu ý rằng thiết kế mới này làm tăng tính mô đun, do đó bạn có thể sử dụng lại các lớp này trong các phần khác của hệ thống (trước khi bạn không thể, các phương thức riêng tư không thể sử dụng lại theo định nghĩa). Đây là lợi thế chính của việc phá vỡ RuleEvaluator, cùng với sự hiểu biết / địa phương tăng lên.
Thử nghiệm sẽ trông cực kỳ giống nhau, ngoại trừ nó thực sự sẽ biên dịch lần này vì GetNextToken()
phương thức này hiện công khai trên Tokenizer
lớp:
TEST_THAT(Tokenizer, canParseSpaceDelimtedTokens)
{
input_string = "1 2 test bar"
tokenizer = Tokenizer(input_string);
ASSERT tokenizer.GetNextToken() IS "1";
ASSERT tokenizer.GetNextToken() IS "2";
ASSERT tokenizer.GetNextToken() IS "test";
ASSERT tokenizer.GetNextToken() IS "bar";
ASSERT tokenizer.HasMoreTokens() IS FALSE;
}
Kiểm tra các thành phần riêng thông qua giao diện công cộng và tránh trùng lặp kiểm tra
Ngay cả khi bạn không nghĩ rằng bạn có thể chia nhỏ vấn đề của mình thành các thành phần mô-đun ít hơn (mà bạn có thể 95% thời gian nếu bạn chỉ cố gắng làm điều đó), bạn chỉ có thể kiểm tra các chức năng riêng tư thông qua giao diện công cộng. Nhiều lần các thành viên tư nhân không đáng để thử nghiệm vì họ sẽ được kiểm tra thông qua giao diện công cộng. Nhiều lần những gì tôi thấy là các bài kiểm tra trông rất giống nhau, nhưng kiểm tra hai hàm / phương thức khác nhau. Điều cuối cùng xảy ra là khi các yêu cầu thay đổi (và chúng luôn luôn thay đổi), giờ đây bạn có 2 bài kiểm tra bị hỏng thay vì 1. Và nếu bạn thực sự kiểm tra tất cả các phương pháp riêng tư của mình, bạn có thể có nhiều hơn 10 bài kiểm tra bị hỏng thay vì 1. Tóm lại , kiểm tra các chức năng riêng tư (bằng cách sử dụngFRIEND_TEST
hoặc đặt chúng ở chế độ công khai hoặc sử dụng sự phản chiếu) có thể được kiểm tra thông qua giao diện chung có thể gây ra sự trùng lặp thử nghiệm . Bạn thực sự không muốn điều này, bởi vì không có gì làm tổn thương nhiều hơn bộ thử nghiệm của bạn làm bạn chậm lại. Nó được cho là để giảm thời gian phát triển và giảm chi phí bảo trì! Nếu bạn kiểm tra các phương thức riêng được kiểm tra theo cách khác thông qua giao diện công cộng, bộ kiểm thử rất có thể làm ngược lại và tích cực tăng chi phí bảo trì và tăng thời gian phát triển. Khi bạn đặt một chức năng riêng tư ở chế độ công khai hoặc nếu bạn sử dụng một cái gì đó như FRIEND_TEST
và / hoặc sự phản chiếu, bạn sẽ thường hối hận về lâu dài.
Xem xét việc thực hiện có thể sau đây của Tokenizer
lớp:
Giả sử có SplitUpByDelimiter()
trách nhiệm trả về một mảng sao cho mỗi phần tử trong mảng là mã thông báo. Hơn nữa, hãy nói rằng đó GetNextToken()
chỉ đơn giản là một trình vòng lặp trên vectơ này. Vì vậy, bài kiểm tra công khai của bạn có thể trông như thế này:
TEST_THAT(Tokenizer, canParseSpaceDelimtedTokens)
{
input_string = "1 2 test bar"
tokenizer = Tokenizer(input_string);
ASSERT tokenizer.GetNextToken() IS "1";
ASSERT tokenizer.GetNextToken() IS "2";
ASSERT tokenizer.GetNextToken() IS "test";
ASSERT tokenizer.GetNextToken() IS "bar";
ASSERT tokenizer.HasMoreTokens() IS false;
}
Hãy giả vờ rằng chúng ta có thứ mà Michael Feather gọi là công cụ dò dẫm . Đây là một công cụ cho phép bạn chạm vào các bộ phận riêng tư của người khác. Một ví dụ là FRIEND_TEST
từ googletest hoặc phản chiếu nếu ngôn ngữ hỗ trợ nó.
TEST_THAT(TokenizerTest, canGenerateSpaceDelimtedTokens)
{
input_string = "1 2 test bar"
tokenizer = Tokenizer(input_string);
result_array = tokenizer.SplitUpByDelimiter(" ");
ASSERT result.size() IS 4;
ASSERT result[0] IS "1";
ASSERT result[1] IS "2";
ASSERT result[2] IS "test";
ASSERT result[3] IS "bar";
}
Chà, bây giờ hãy nói rằng các yêu cầu thay đổi và việc token hóa trở nên phức tạp hơn nhiều. Bạn quyết định rằng một dấu phân cách chuỗi đơn giản sẽ không đủ và bạn cần một Delimiter
lớp để xử lý công việc. Đương nhiên, bạn sẽ mong đợi một thử nghiệm bị phá vỡ, nhưng nỗi đau đó tăng lên khi bạn kiểm tra các chức năng riêng tư.
Khi nào có thể thử nghiệm phương pháp riêng tư là phù hợp?
Không có "một kích thước phù hợp với tất cả" trong phần mềm. Đôi khi không sao (và thực sự lý tưởng) để "phá vỡ các quy tắc". Tôi mạnh mẽ ủng hộ không kiểm tra chức năng riêng tư khi bạn có thể. Có hai tình huống chính khi tôi nghĩ nó ổn:
Tôi đã làm việc rộng rãi với các hệ thống cũ (đó là lý do tại sao tôi là một fan hâm mộ lớn của Michael Feathers) và tôi có thể nói một cách an toàn rằng đôi khi chỉ đơn giản là an toàn nhất khi chỉ kiểm tra chức năng riêng tư. Nó có thể đặc biệt hữu ích cho việc đưa "các bài kiểm tra đặc tính" vào đường cơ sở.
Bạn đang vội, và phải làm điều nhanh nhất có thể ở đây và bây giờ. Về lâu dài, bạn không muốn thử nghiệm các phương pháp riêng tư. Nhưng tôi sẽ nói rằng thường phải mất một thời gian để cấu trúc lại để giải quyết các vấn đề thiết kế. Và đôi khi bạn phải vận chuyển trong một tuần. Không sao đâu: làm nhanh và bẩn và kiểm tra các phương pháp riêng tư bằng cách sử dụng công cụ dò dẫm nếu đó là cách bạn nghĩ là cách nhanh nhất và đáng tin cậy nhất để hoàn thành công việc. Nhưng hãy hiểu rằng những gì bạn đã làm là không tối ưu trong thời gian dài, và vui lòng xem xét quay lại với nó (hoặc, nếu nó bị quên nhưng bạn sẽ thấy nó sau, hãy sửa nó).
Có lẽ có những tình huống khác mà nó ổn. Nếu bạn nghĩ rằng nó ổn, và bạn có một lời biện minh tốt, thì hãy làm điều đó. Không ai được ngăn cản bạn. Chỉ cần nhận thức được các chi phí tiềm năng.
Lý do TDD
Bên cạnh đó, tôi thực sự không thích mọi người sử dụng TDD như một cái cớ để thử nghiệm các phương pháp riêng tư. Tôi thực hành TDD và tôi không nghĩ TDD buộc bạn phải làm điều này. Bạn có thể viết thử nghiệm của mình (cho giao diện công cộng của bạn) trước, sau đó viết mã để đáp ứng giao diện đó. Đôi khi tôi viết một bài kiểm tra cho giao diện công cộng và tôi cũng sẽ thỏa mãn nó bằng cách viết một hoặc hai phương thức riêng tư nhỏ hơn (nhưng tôi không kiểm tra trực tiếp các phương thức riêng tư, nhưng tôi biết chúng hoạt động hoặc thử nghiệm công khai của tôi sẽ thất bại ). Nếu tôi cần kiểm tra các trường hợp cạnh của phương pháp riêng tư đó, tôi sẽ viết một loạt các bài kiểm tra sẽ đánh chúng qua giao diện công cộng của tôi.Nếu bạn không thể tìm ra cách đánh vào các trường hợp cạnh, đây là một dấu hiệu mạnh bạn cần cấu trúc lại thành các thành phần nhỏ với các phương thức công khai của riêng chúng. Đó là một dấu hiệu các chức năng riêng tư của bạn đang hoạt động quá nhiều và nằm ngoài phạm vi của lớp .
Ngoài ra, đôi khi tôi thấy tôi viết một bài kiểm tra quá lớn để cắn vào lúc này và vì vậy tôi nghĩ rằng "sau này tôi sẽ quay lại bài kiểm tra đó khi tôi có nhiều API hơn để làm việc" (Tôi Tôi sẽ bình luận và giữ nó trong tâm trí tôi). Đây là nơi có rất nhiều nhà phát triển mà tôi đã gặp sau đó sẽ bắt đầu viết bài kiểm tra cho chức năng riêng tư của họ, sử dụng TDD làm vật tế thần. Họ nói "ồ, tôi cần một số thử nghiệm khác, nhưng để viết thử nghiệm đó, tôi sẽ cần các phương thức riêng tư này. Vì vậy, vì tôi không thể viết bất kỳ mã sản xuất nào mà không viết thử nghiệm, tôi cần phải viết thử nghiệm cho một phương pháp riêng tư. " Nhưng những gì họ thực sự cần làm là tái cấu trúc thành các thành phần nhỏ hơn và có thể tái sử dụng thay vì thêm / kiểm tra một loạt các phương thức riêng tư vào lớp hiện tại của họ.
Ghi chú:
Tôi đã trả lời một câu hỏi tương tự về việc thử nghiệm các phương thức riêng tư bằng GoogleTest một lúc trước. Tôi hầu như đã sửa đổi câu trả lời đó thành thuyết bất khả tri ngôn ngữ hơn ở đây.
PS Đây là bài giảng có liên quan về các lớp băng và công cụ mò mẫm của Michael Feathers: https://www.youtube.com/watch?v=4cVZvoFGJTU