Một hoặc nhiều tệp để kiểm tra đơn vị một lớp?


20

Khi nghiên cứu các thực tiễn tốt nhất để kiểm tra đơn vị để giúp tập hợp các hướng dẫn cho tổ chức của mình, tôi đã đặt ra câu hỏi là nên tách riêng các đồ đạc thử nghiệm (các lớp kiểm tra) hay giữ tất cả các bài kiểm tra cho một lớp trong một tệp.

Fwiw, tôi đang đề cập đến "các bài kiểm tra đơn vị" theo nghĩa thuần túy rằng chúng là các bài kiểm tra hộp trắng nhắm vào một lớp duy nhất, một xác nhận cho mỗi bài kiểm tra, tất cả các phụ thuộc bị chế giễu, v.v.

Một kịch bản ví dụ là một lớp (gọi nó là Tài liệu) có hai phương thức: CheckIn và CheckOut. Mỗi phương thức thực hiện các quy tắc khác nhau, vv kiểm soát hành vi của họ. Theo quy tắc một xác nhận cho mỗi thử nghiệm, tôi sẽ có nhiều thử nghiệm cho mỗi phương pháp. Tôi có thể đặt tất cả các bài kiểm tra trong một DocumentTestslớp duy nhất với tên như CheckInShouldThrowExceptionWhenUserIsUnauthorizedCheckOutShouldThrowExceptionWhenUserIsUnauthorized.

Hoặc, tôi có thể có hai lớp kiểm tra riêng biệt: CheckInShouldCheckOutShould. Trong trường hợp này, tên thử nghiệm của tôi sẽ được rút ngắn nhưng chúng sẽ được sắp xếp để tất cả các thử nghiệm cho một hành vi (phương pháp) cụ thể được kết hợp với nhau.

Tôi chắc chắn có những người ủng hộ và tiếp cận và tự hỏi liệu có ai đã đi theo con đường với nhiều tệp không và nếu có thì tại sao? Hoặc, nếu bạn đã chọn cách tiếp cận tập tin duy nhất, tại sao bạn cảm thấy nó tốt hơn?


2
Tên phương pháp thử nghiệm của bạn quá dài. Đơn giản hóa chúng, ngay cả khi nó có nghĩa là một phương thức thử nghiệm sẽ chứa nhiều xác nhận.
Bernard

12
@Bernard: được coi là thực hành xấu khi có nhiều xác nhận cho mỗi phương thức, tuy nhiên tên phương thức thử nghiệm dài KHÔNG được coi là thực tiễn xấu. Chúng tôi thường ghi lại những gì phương thức đang thử nghiệm trong chính tên của nó. Ví dụ: constructor_nullSomeList (), setUserName_UserNameIsNotValid () v.v ...
c_maker

4
@Bernard: tôi cũng sử dụng tên thử nghiệm dài (và chỉ có ở đó!). Tôi yêu họ, vì nó quá rõ ràng những gì không hoạt động (nếu tên của bạn là lựa chọn tốt;)).
Sebastian Bauer

Nhiều khẳng định không phải là một thực tiễn xấu, nếu tất cả đều kiểm tra cùng một kết quả. Ví dụ. bạn sẽ không có testResponseContainsSuccessTrue(), testResponseContainsMyData()testResponseStatusCodeIsOk(). Bạn sẽ có chúng trong một đĩa đơn testResponse()trong đó có ba khẳng định: assertEquals(200, response.status), assertEquals({"data": "mydata"}, response.data)assertEquals(true, response.success)
Juha Untinen

Câu trả lời:


18

Điều này rất hiếm, nhưng đôi khi thật hợp lý khi có nhiều lớp kiểm tra cho một lớp nhất định đang được kiểm tra. Thông thường tôi sẽ làm điều này khi các thiết lập khác nhau được yêu cầu và được chia sẻ trên một tập hợp con của các bài kiểm tra.


3
Ví dụ tốt tại sao cách tiếp cận đó được trình bày cho tôi ở nơi đầu tiên. Chẳng hạn, khi tôi muốn kiểm tra phương thức CheckIn, tôi luôn muốn đối tượng được kiểm tra được thiết lập một chiều nhưng khi tôi kiểm tra phương thức CheckOut, tôi yêu cầu một thiết lập khác. Điểm tốt.
SonOfPirate

14

Thực sự không thể thấy bất kỳ lý do thuyết phục nào tại sao bạn lại chia một bài kiểm tra cho một lớp thành nhiều lớp kiểm tra. Vì ý tưởng lái xe nên duy trì sự gắn kết ở cấp độ lớp, bạn cũng nên cố gắng đạt được nó ở cấp độ thử nghiệm. Chỉ là một số lý do ngẫu nhiên:

  1. Không cần sao chép (và duy trì nhiều phiên bản) mã thiết lập
  2. Dễ dàng hơn để chạy tất cả các bài kiểm tra cho một lớp từ IDE nếu chúng thuộc nhóm trong một lớp kiểm tra
  3. Xử lý sự cố dễ dàng hơn với ánh xạ một-một giữa các lớp kiểm tra và các lớp.

Điểm tốt. Nếu lớp đang thử nghiệm là một loại bùn khủng khiếp, tôi sẽ không loại trừ một số lớp thử nghiệm, trong đó một số lớp chứa logic của trình trợ giúp. Tôi chỉ không thích các quy tắc rất cứng nhắc. Khác hơn thế, điểm tuyệt vời.
Công việc

1
Tôi sẽ loại bỏ mã thiết lập trùng lặp bằng cách có một lớp cơ sở chung cho các đồ đạc thử nghiệm hoạt động với một lớp duy nhất. Không chắc chắn tôi đồng ý với hai điểm khác.
SonOfPirate

Trong JUnit, ví dụ, bạn có thể muốn kiểm tra mọi thứ bằng cách sử dụng các Runners khác nhau dẫn đến nhu cầu về các lớp khác nhau.
Joachim Nilsson

Khi tôi viết một lớp học với một số lượng lớn các phương pháp với toán học phức tạp trong mỗi phương pháp, tôi thấy tôi có 85 bài kiểm tra và chỉ có các bài kiểm tra viết cho 24% các phương pháp cho đến nay. Tôi thấy mình ở đây bởi vì tôi đã tự hỏi nếu nó điên rồ khi chia số lượng lớn các bài kiểm tra này thành các loại riêng biệt bằng cách nào đó để tôi có thể chạy các tập con.
Vịt Mooing

7

Nếu bạn bị buộc phải phân tách các bài kiểm tra đơn vị cho một lớp trên nhiều tệp, đó có thể là một dấu hiệu cho thấy chính lớp đó được thiết kế kém. Tôi không thể nghĩ ra bất kỳ kịch bản nào trong đó việc phân chia các bài kiểm tra đơn vị cho một lớp tuân thủ hợp lý Nguyên tắc Trách nhiệm duy nhất và các thực tiễn tốt nhất về lập trình khác sẽ có ích hơn.

Hơn nữa, có tên phương thức dài hơn trong các bài kiểm tra đơn vị là chấp nhận được, nhưng nếu điều đó làm phiền bạn, bạn luôn có thể suy nghĩ lại về quy ước đặt tên bài kiểm tra đơn vị của mình để rút ngắn tên.


Than ôi, trong thế giới thực, không phải tất cả các lớp đều tuân thủ SRP và cộng sự ... và điều này trở thành một vấn đề khi bạn đang kiểm tra mã kế thừa đơn vị.
Péter Török

3
Không chắc chắn làm thế nào SRP liên quan ở đây. Tôi có nhiều phương thức trong lớp và tôi cần viết các bài kiểm tra đơn vị cho tất cả các yêu cầu khác nhau thúc đẩy hành vi của chúng. Có tốt hơn không khi có một lớp kiểm tra với 50 phương thức kiểm tra hoặc 5 lớp kiểm tra với 10 lớp kiểm tra, mỗi lớp liên quan đến một phương thức cụ thể trong lớp được kiểm tra?
SonOfPirate

2
@SonOfPirate: Nếu bạn cần rất nhiều bài kiểm tra, điều đó có thể có nghĩa là phương pháp của bạn đang làm quá nhiều. Vì vậy, SRP có liên quan rất nhiều ở đây. Nếu bạn áp dụng SRP cho các lớp cũng như các phương thức, bạn sẽ có rất nhiều lớp và phương thức nhỏ dễ kiểm tra và bạn sẽ không cần quá nhiều phương thức kiểm tra. (Nhưng tôi đồng ý với Péter Török rằng khi bạn kiểm tra mã kế thừa, các thực tiễn tốt sẽ ra khỏi cửa và bạn chỉ cần làm tốt nhất có thể với những gì bạn được trao ...)
c_maker

Ví dụ tốt nhất tôi có thể đưa ra là phương thức CheckIn trong đó chúng ta có các quy tắc kinh doanh xác định ai được phép thực hiện hành động (ủy quyền), nếu đối tượng ở trạng thái thích hợp để được kiểm tra như nếu nó được kiểm tra và nếu thay đổi đa được tạo ra. Chúng tôi sẽ có các kiểm tra đơn vị xác minh các quy tắc ủy quyền đang được thi hành cũng như các kiểm tra để xác minh rằng phương thức này hoạt động chính xác nếu CheckIn được gọi khi đối tượng không được kiểm tra, không thay đổi, v.v. Đó là lý do tại sao chúng tôi kết thúc với nhiều xét nghiệm. Bạn đang đề nghị điều này là sai?
SonOfPirate

Tôi không nghĩ rằng có những câu trả lời khó và nhanh, đúng hay sai về chủ đề này. Nếu những gì bạn đang làm là làm việc cho bạn, điều đó thật tuyệt. Đây chỉ là quan điểm của tôi về một hướng dẫn chung.
FishBasketGordo

2

Một trong những lập luận của tôi chống lại việc tách các bài kiểm tra thành nhiều lớp là các nhà phát triển khác trong nhóm trở nên khó khăn hơn (đặc biệt là những người không am hiểu về kiểm tra) để xác định các bài kiểm tra hiện tại ( Gee tôi tự hỏi liệu đã có bài kiểm tra này chưa Tôi tự hỏi nó sẽ ở đâu? ) và cũng là nơi để đặt các bài kiểm tra mới ( Tôi sẽ viết một bài kiểm tra cho phương thức này trong lớp này, nhưng không chắc chắn liệu tôi nên đặt chúng vào một tệp mới, hoặc một tệp hiện có một? )

Trong trường hợp các thử nghiệm khác nhau đòi hỏi các thiết lập khác nhau mạnh mẽ, tôi đã thấy một số học viên của TDD đặt "vật cố" hoặc "thiết lập" trong các lớp / tệp khác nhau, nhưng không phải là các thử nghiệm.


1

Tôi ước tôi có thể nhớ / tìm liên kết nơi kỹ thuật tôi chọn áp dụng lần đầu tiên được trình diễn. Về cơ bản, tôi tạo một lớp trừu tượng duy nhất cho mỗi lớp được kiểm tra có chứa các đồ đạc kiểm tra lồng nhau (các lớp) cho mỗi thành viên đang được kiểm tra. Điều này cung cấp sự phân tách ban đầu mong muốn nhưng giữ tất cả các thử nghiệm trong cùng một tệp cho từng vị trí. Ngoài ra, điều này tạo ra một tên lớp cho phép dễ dàng phân nhóm và sắp xếp trong trình chạy thử nghiệm.

Dưới đây là một ví dụ về cách tiếp cận này được áp dụng cho kịch bản ban đầu của tôi:

public abstract class DocumentTests : TestBase
{
    [TestClass()]
    public sealed class CheckInShould : DocumentTests
    {
        [TestMethod()]
        public void ThrowExceptionWhenUserIsNotAuthorized()
        {
        }
    }

    [TestClass()]
    public sealed class CheckOutShould : DocumentTests
    {
        [TestMethod()]
        public void ThrowExceptionWhenUserIsNotAuthorized()
        {
        }
    }
}

Điều này dẫn đến các thử nghiệm sau đây xuất hiện trong danh sách thử nghiệm:

DocumentTests+CheckInShould.ThrowExceptionWhenUserIsNotAuthorized
DocumentTests+CheckOutShould.ThrowExceptionWhenUserIsNotAuthorized

Khi nhiều bài kiểm tra được thêm vào, chúng dễ dàng được nhóm và sắp xếp theo tên lớp, nó cũng giữ tất cả các bài kiểm tra cho lớp được kiểm tra được liệt kê cùng nhau.

Một ưu điểm khác của cách tiếp cận này mà tôi đã học được để tận dụng là bản chất hướng đối tượng của cấu trúc này có nghĩa là tôi có thể định nghĩa mã bên trong các phương thức khởi động và dọn dẹp của lớp cơ sở DocumentTests sẽ được chia sẻ bởi tất cả các lớp lồng nhau cũng như bên trong mỗi lớp lớp lồng nhau cho các bài kiểm tra nó chứa.


1

Cố gắng nghĩ cách khác trong một phút.

Tại sao bạn chỉ có một lớp kiểm tra mỗi lớp? Bạn đang làm bài kiểm tra lớp hay bài kiểm tra đơn vị? Bạn thậm chí phụ thuộc vào các lớp học ?

Một bài kiểm tra đơn vị được cho là đang kiểm tra một hành vi cụ thể, trong một bối cảnh cụ thể. Thực tế là lớp của bạn đã có một CheckInphương tiện nên có một hành vi cần nó ngay từ đầu.

Bạn nghĩ thế nào về mã giả này:

// check_in_test.file

class CheckInTest extends TestCase {
        /** @test */
        public function unauthorized_users_cannot_check_in() {
                $this->expectException();

                $document = new Document($unauthorizedUser);

                $document->checkIn();
        }
}

Bây giờ bạn không trực tiếp thử nghiệm checkInphương pháp. Thay vào đó, bạn đang kiểm tra một hành vi (đó là để kiểm tra một cách may mắn;)) và nếu bạn cần cấu trúc lại Documentvà chia nó thành các lớp khác nhau hoặc hợp nhất một lớp khác vào đó, các tệp kiểm tra của bạn vẫn mạch lạc, chúng vẫn có ý nghĩa bạn không bao giờ thay đổi logic khi tái cấu trúc, chỉ cấu trúc.

Một tệp kiểm tra cho một lớp chỉ đơn giản là làm cho việc tái cấu trúc khó hơn bất cứ khi nào bạn cần và các kiểm tra cũng ít có ý nghĩa hơn về mặt miền / logic của chính mã.


0

Nói chung, tôi khuyên bạn nên suy nghĩ về các bài kiểm tra như kiểm tra một hành vi của lớp hơn là một phương thức. Tức là một số bài kiểm tra của bạn có thể cần gọi cả hai phương thức trên lớp để kiểm tra một số hành vi dự kiến.

Thông thường tôi bắt đầu với một lớp thử nghiệm đơn vị cho mỗi lớp sản xuất, nhưng cuối cùng có thể chia lớp thử nghiệm đơn vị đó thành nhiều lớp thử nghiệm dựa trên hành vi mà chúng thử nghiệm. Nói cách khác, tôi khuyên bạn không nên tách lớp thử nghiệm thành CheckInShould và CheckOutShould, nhưng thay vào đó là phân tách theo hành vi của đơn vị được thử nghiệm.


0

1. Xem xét những gì có khả năng đi sai

Trong quá trình phát triển ban đầu, bạn biết khá rõ những gì bạn đang làm và cả hai giải pháp có thể sẽ hoạt động tốt.

Sẽ thú vị hơn khi một bài kiểm tra thất bại sau một thay đổi nhiều sau đó. Có hai khả năng:

  1. Bạn đã bị hỏng nghiêm trọng CheckIn(hoặc CheckOut). Một lần nữa, cả giải pháp một tập tin cũng như hai tập tin đều ổn trong trường hợp đó.
  2. Bạn đã sửa đổi cả hai CheckInCheckOut(và có lẽ các bài kiểm tra của họ) theo cách có ý nghĩa đối với từng người trong số họ, nhưng không phải cho cả hai cùng nhau. Bạn đã phá vỡ sự gắn kết của cặp. Trong trường hợp đó, việc chia các bài kiểm tra qua hai tệp sẽ khiến việc hiểu vấn đề trở nên khó khăn hơn.

2. Xem xét những bài kiểm tra được sử dụng cho

Các thử nghiệm phục vụ hai mục đích chính:

  1. Tự động kiểm tra chương trình vẫn hoạt động chính xác. Cho dù các bài kiểm tra nằm trong một tệp hoặc một số không quan trọng cho mục đích này.
  2. Giúp một người đọc có ý nghĩa của chương trình. Điều này sẽ dễ dàng hơn nhiều nếu các bài kiểm tra cho chức năng liên quan chặt chẽ được giữ cùng nhau, bởi vì thấy sự gắn kết của chúng là một con đường nhanh chóng để hiểu.

Vì thế?

Cả hai quan điểm này đề nghị giữ các bài kiểm tra cùng nhau có thể giúp ích, nhưng sẽ không bị tổn thươ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.