Kết nối mã ứng dụng với các bài kiểm tra đơn vị


8

Tôi đang làm việc trên một dự án mà chúng tôi phải thực hiện và thử nghiệm một số mô-đun mới. Tôi có một kiến ​​trúc khá rõ ràng trong đầu nên tôi đã nhanh chóng viết ra các lớp và phương thức chính và sau đó chúng tôi bắt đầu viết các bài kiểm tra đơn vị.

Trong khi viết các bài kiểm tra, chúng tôi đã phải thực hiện một vài sửa đổi cho mã gốc, chẳng hạn như

  • Công khai các phương thức riêng tư để kiểm tra chúng
  • Thêm phương thức bổ sung để truy cập các biến riêng tư
  • Thêm các phương thức bổ sung để tiêm các đối tượng giả nên được sử dụng khi mã chạy bên trong một bài kiểm tra đơn vị.

Bằng cách nào đó tôi có cảm giác rằng đây là những triệu chứng mà chúng ta đang làm sai, ví dụ

  1. thiết kế ban đầu đã sai (một số chức năng nên được công khai ngay từ đầu),
  2. mã không được thiết kế đúng để được giao tiếp với các bài kiểm tra đơn vị (có thể do thực tế là chúng tôi đã bắt đầu thiết kế các bài kiểm tra đơn vị khi đã có một vài lớp được thiết kế),
  3. chúng tôi đang triển khai kiểm tra đơn vị sai cách (ví dụ: kiểm tra đơn vị chỉ nên kiểm tra trực tiếp / giải quyết các phương thức công khai của API, không phải phương thức riêng tư),
  4. một hỗn hợp của ba điểm trên và có thể một số vấn đề bổ sung mà tôi chưa nghĩ đến.

Vì tôi có một số kinh nghiệm về kiểm thử đơn vị nhưng tôi không phải là một bậc thầy, tôi sẽ rất thích đọc suy nghĩ của bạn về những vấn đề này.

Bên cạnh các câu hỏi chung ở trên, tôi có một số câu hỏi kỹ thuật cụ thể hơn:

Câu hỏi 1. Có ý nghĩa gì khi trực tiếp kiểm tra một phương thức riêng m của lớp A và thậm chí công khai nó để kiểm tra nó không? Hoặc tôi nên cho rằng m được kiểm tra gián tiếp bằng các bài kiểm tra đơn vị bao gồm các phương thức công khai khác gọi cho m?

Câu hỏi 2. Nếu một thể hiện của lớp A chứa một thể hiện của lớp B (tập hợp tổng hợp), nó có hợp lý để giả định B để kiểm tra A không? Ý tưởng đầu tiên của tôi là tôi không nên chế giễu B vì thể hiện B là một phần của thể hiện A, nhưng sau đó tôi bắt đầu nghi ngờ về điều này. Đối số của tôi chống lại việc chế nhạo B cũng giống như đối với 1: B là wrt riêng tư A và chỉ được sử dụng để thực hiện nó, do đó, chế giễu B có vẻ như tôi đang tiết lộ các chi tiết riêng tư của A như trong (1). Nhưng có lẽ những vấn đề này cho thấy một lỗ hổng thiết kế: có thể chúng ta không nên sử dụng kết hợp tổng hợp mà là một liên kết đơn giản từ A đến B.

Câu hỏi 3. Trong ví dụ trên, nếu chúng ta quyết định giả định B, làm thế nào để chúng ta tiêm cá thể B vào A? Dưới đây là một số ý tưởng chúng tôi đã có:

  • Đặt đối tượng B làm đối số cho hàm tạo A thay vì tạo đối tượng B trong hàm tạo A.
  • Truyền giao diện BFactory làm đối số cho hàm tạo A và cho phép A sử dụng nhà máy để tạo cá thể B riêng của nó.
  • Sử dụng một đơn vị BFactory là riêng tư để A. Sử dụng phương thức tĩnh A :: setBFactory () để đặt đơn vị. Khi A muốn tạo cá thể B, nó sử dụng singleton của nhà máy nếu nó được đặt (kịch bản thử nghiệm), nó tạo B trực tiếp nếu singleton không được đặt (kịch bản mã sản xuất).

Hai lựa chọn đầu tiên có vẻ sạch hơn đối với tôi, nhưng chúng yêu cầu thay đổi chữ ký của nhà xây dựng A: thay đổi API chỉ để làm cho nó dễ kiểm tra hơn có vẻ khó xử với tôi, đây có phải là một thông lệ không?

Cái thứ ba có một ưu điểm là nó không yêu cầu thay đổi chữ ký của hàm tạo (thay đổi API ít xâm lấn hơn), nhưng nó yêu cầu gọi phương thức tĩnh setBFactory () trước khi bắt đầu thử nghiệm, dễ bị lỗi IMO ( phụ thuộc ngầm vào một cuộc gọi phương thức để các thử nghiệm hoạt động đúng). Vì vậy, tôi không biết chúng ta nên chọn cái nào.


Tôi nghĩ rằng tính năng lớp / chức năng bạn bè của C ++ có thể được sử dụng. Bạn đã thử chưa?
Mert Akcakaya

@Mert: Chúng tôi chưa thử. Câu hỏi: Bằng cách sử dụng bạn bè, chúng tôi sẽ phải khai báo các lớp mã kiểm tra là bạn của các lớp mã chính. Được không Chúng tôi sẽ có mã sản xuất tùy thuộc vào mã thử nghiệm. Đây có phải là một ý tưởng tốt? Hay đó là một giải pháp khác mà bạn có trong tâm trí?
Giorgio

Tôi không phải là một chuyên gia về C ++, nó chỉ xuất hiện trong đầu tôi như một giải pháp đơn giản.
Mert Akcakaya

Câu trả lời:


8

Tôi nghĩ rằng thử nghiệm các phương pháp công cộng là đủ hầu hết thời gian.

Nếu bạn có sự phức tạp lớn trong các phương thức riêng tư của mình, thì hãy xem xét đưa chúng vào một lớp khác làm phương thức công khai và sử dụng chúng như các cuộc gọi riêng tư đến các phương thức đó trong lớp ban đầu của bạn. Bằng cách này, bạn có thể đảm bảo cả hai phương thức trong lớp gốc và lớp tiện ích của bạn hoạt động chính xác.

Dựa nhiều vào các phương pháp riêng tư là điều cần xem xét về các thiết kế.


Tôi đồng ý với bạn: nếu một người cảm thấy cần phải thử nghiệm một phương thức riêng tư, thì có lẽ nó không nên là riêng tư ngay từ đầu và người ta nên đặt nó trong một lớp tiện ích riêng biệt cần được kiểm tra riêng.
Giorgio

Kiểm tra tất cả các phương thức công cộng nên đã kiểm tra (đọc: bao gồm ) tất cả các phương thức riêng tư. Khác, họ đang làm gì ở đó? :)
Amadeus Hein

1
@AADEus Heing, Kiểm tra các phương thức công khai chỉ có thể gọi các phương thức riêng tư, không kiểm tra chúng.
Mert Akcakaya

2
@Mert Có, nhưng nói chung, muốn thử nghiệm một phương thức riêng tư là một tín hiệu có gì đó không đúng trong mã. Chi tiết hơn: liên kết
Amadeus Hein

1
"Dựa nhiều vào các phương thức riêng tư là điều cần xem xét về các mong muốn thiết kế.": Trong trường hợp của chúng tôi, các phương thức riêng là các phương thức tiện ích được gọi một lần bởi một phương thức công khai. Nhưng sau đó chúng tôi tự hỏi liệu nó có mạnh mẽ hơn để kiểm tra chúng không. Nhưng có lẽ, như nhiều người đã chỉ ra, sẽ rất hợp lý khi chuyển các phương thức này vào một lớp tiện ích và biến chúng thành công khai nếu chúng quan trọng đến mức người ta muốn kiểm tra chúng.
Giorgio

5

Câu hỏi 1: Điều đó phụ thuộc. Thông thường, bạn bắt đầu với các bài kiểm tra đơn vị cho các phương pháp công cộng. Đôi khi bạn gặp phải một phương pháp m bạn muốn giữ riêng tư cho A, nhưng bạn nghĩ rằng việc kiểm tra m một cách hợp lý cũng có ý nghĩa. Nếu đó là trường hợp, bạn nên đặt m công khai hoặc biến lớp kiểm tra thành lớp TestAbạn bè A. Nhưng hãy cẩn thận, việc thêm một bài kiểm tra đơn vị để kiểm tra m làm cho việc thay đổi chữ ký hoặc hành vi của m sau đó trở nên khó khăn hơn một chút; nếu bạn muốn giữ "chi tiết triển khai" của A, tốt hơn là không nên thêm kiểm tra đơn vị trực tiếp.

Câu hỏi 2: Tập hợp tổng hợp (tích hợp C ++) không hoạt động tốt khi nói đến một ví dụ. Trong thực tế, vì việc xây dựng B xảy ra ngầm trong hàm tạo của A, bạn không có cơ hội tiêm phụ thuộc từ bên ngoài. Nếu đó là một vấn đề phụ thuộc vào cách bạn muốn kiểm tra A: nếu bạn nghĩ rằng việc kiểm tra A một cách có ý nghĩa hơn, với một giả B thay vì B, tốt hơn nên sử dụng một liên kết đơn giản. Nếu bạn nghĩ rằng bạn có thể viết tất cả các bài kiểm tra đơn vị cần thiết cho A mà không cần chế nhạo B, thì một hỗn hợp có thể sẽ ổn.

Câu hỏi 3: thay đổi API để làm cho mọi thứ dễ kiểm tra hơn là phổ biến miễn là bạn không có nhiều mã dựa trên API đó. Khi bạn đang thực hiện TDD, sau đó bạn không phải thay đổi API để làm cho mọi thứ dễ kiểm tra hơn, ban đầu bạn bắt đầu với một API được thiết kế để kiểm tra. Nếu bạn muốn thay đổi API sau này để khiến mọi thứ dễ kiểm tra hơn, bạn có thể gặp phải sự cố, đó là sự thật. Vì vậy, tôi sẽ sử dụng giải pháp thay thế thứ nhất hoặc thứ hai mà bạn đã mô tả miễn là bạn có thể thay đổi API của mình một cách dễ dàng và sử dụng cái gì đó như phương án thứ ba của bạn (nhận xét: điều này cũng hoạt động mà không có mẫu đơn) như là phương sách cuối cùng nếu tôi phải không thay đổi API trong mọi trường hợp.

Về những lo ngại của bạn rằng bạn có thể "làm sai": mọi động cơ hoặc máy móc lớn đều có các lỗ mở bảo trì, vì vậy IMHO nhu cầu thêm một cái gì đó như thế vào phần mềm không quá ngạc nhiên.


2
+1 Cho đoạn cuối. Đối với một nghiên cứu exapmple tốt làm thế nào các thế giới điện tử kiểm tra?
mattnz

+1: Cảm ơn câu trả lời rất có động lực. Một trong những mối quan tâm chính của tôi là mã ứng dụng nên cung cấp chức năng cho mã ứng dụng, không phải để kiểm tra mã: mã kiểm tra phải tuân theo mã ứng dụng mà không áp đặt các yêu cầu trong đó. Tất nhiên, bạn có thể có một số yêu cầu để làm cho mã dễ quan sát hơn, nhưng những điều này nên thực sự tối thiểu. Xem ví dụ tổng hợp: IMO người ta nên chọn liên kết đơn giản bằng văn bản dựa trên các yêu cầu miền ứng dụng, chứ không phải dựa trên khả năng kiểm tra. Yêu cầu ứng dụng uốn IMO để yêu cầu thử nghiệm phải là giải pháp cuối cùng.
Giorgio

1
@Giorgio: quan niệm sai lầm của bạn ở đây là việc sử dụng kết hợp với kết hợp có liên quan đến các yêu cầu miền - bạn có thể thực hiện bất kỳ yêu cầu miền nào với cả hai loại thiết kế. Làm cho phần mềm dễ kiểm tra hơn không phải là điều bạn có thể mong đợi đạt được chỉ bằng cách thực hiện các thay đổi tối thiểu. Nếu bạn làm đúng, nó chắc chắn sẽ ảnh hưởng đến phần mềm của bạn ở mức thiết kế.
Doc Brown

@Doc Brown: Chà, nếu A <> - B là hợp số, các thể hiện của B chỉ có thể tồn tại nếu được quản lý bởi các thể hiện của A và thời gian tồn tại của chúng được kiểm soát theo thời gian của A. Đây có thể là một yêu cầu miền. Mặt khác, một liên kết "sử dụng" đơn giản A -> B không áp đặt rằng một thể hiện A nên quản lý một thể hiện B. Có lẽ trong trường hợp của chúng tôi thực sự có một lỗ hổng trong phân tích miền ứng dụng (chúng ta nên sử dụng một liên kết thay vì thành phần).
Giorgio

@Giorgio: bạn có thể có yêu cầu cho các trường hợp của A và B có cùng thời gian tồn tại. Nhưng làm thế nào bạn hoàn thành nó là của bạn, không có yêu cầu tên miền nào buộc bạn phải giải quyết vấn đề này bằng cách sử dụng hình thức sáng tác tích hợp C ++. Nếu bạn muốn có thể kiểm tra A và B một cách cô lập, thì - ít nhất là đối với thử nghiệm của bạn - bạn cần phải có một phiên bản của A mà không có B và ngược lại. Vì vậy, trong trường hợp này, tốt hơn bạn nên sử dụng cơ chế thời gian chạy để kiểm soát tuổi thọ của các đối tượng đó (ví dụ: sử dụng con trỏ thông minh) thay vì cơ chế thời gian biên dịch.
Doc Brown

1

Bạn nên tra cứu Dependency Injection và Inversion of Control. Misko Hevery giải thích rất nhiều về nó trên blog của mình. DI và IoC thực sự là một mẫu thiết kế để tạo mã dễ dàng kiểm tra đơn vị và giả định.

Câu hỏi 1: Không, không công khai các phương thức riêng tư để kiểm tra chúng. Nếu phương thức đủ phức tạp, bạn có thể tạo một lớp cộng tác viên chỉ chứa phương thức đó và đưa nó (truyền vào hàm tạo) vào lớp khác của bạn. 1

Câu 2: Ai xây dựng A trong trường hợp này? Nếu bạn có một lớp nhà máy / nhà xây dựng xây dựng A, thì việc chuyển cộng tác viên B vào nhà xây dựng sẽ không có hại gì. Nếu A nằm trong một gói / không gian tên riêng biệt với mã sử dụng nó, bạn thậm chí có thể đặt gói xây dựng ở chế độ riêng tư và làm cho nó để nhà máy / nhà xây dựng là lớp duy nhất có thể xây dựng nó.

Câu hỏi 3: Tôi đã trả lời điều này trong Câu hỏi 2. Một số lưu ý thêm:

  • Sử dụng một mẫu xây dựng / nhà máy cho phép bạn thực hiện tiêm phụ thuộc bao nhiêu tùy ý, mà không phải lo lắng về việc tạo mã bằng cách sử dụng lớp của bạn khó sử dụng.
  • Nó ngăn cách thời gian thi công đối tượng từ thời gian sử dụng đối tượng có nghĩa là mã sử dụng API của bạn có thể đơn giản hơn.

1 Đây là câu trả lời C # / Java - C ++ có thể có các tính năng bổ sung tạo điều kiện thuận lợi cho việc này

Như một câu trả lời cho ý kiến ​​của bạn:

Ý tôi là mã sản xuất của bạn sẽ được thay đổi từ (xin vui lòng tha thứ cho giả của tôi C ++):

void MyClass::MyUseOfA()
{
  A* a = new A();
  a->SomeMethod();
}

A::A()
{
  m_b = new B();
}

đến:

void MyClass::MyUseOfA()
{
  A* a = AFactory.NewA();
  a->SomeMethod();
}

A* AFactory::NewA()
{
  // Construct dependencies
  B* b = new B();
  return new A(b);
}

A::A(B* b)
{
  m_b = b;
}

Sau đó, bài kiểm tra của bạn có thể là:

void MyTest::TestA()
{
  MockB* b = new MockB();
  b->SetSomethingInteresting(somethingInteresting);

  A* a = new A(b);

  a->DoSomethingInteresting();

  b->DidSomethingInterestingHappen();
}

Theo cách này, bạn không cần phải vượt qua nhà máy, mã gọi A không cần biết cách xây dựng A và thử nghiệm có thể tùy chỉnh cấu trúc A để cho phép kiểm tra hành vi.

Trong bình luận khác của bạn, bạn đã hỏi về sự phụ thuộc lồng nhau. Vì vậy, ví dụ, nếu phụ thuộc của bạn là:

A -> C -> D -> B

Câu hỏi đầu tiên được đặt ra là nếu A sử dụng C và D. Nếu không, tại sao chúng lại được đưa vào A? Giả sử chúng được sử dụng, thì có lẽ cần phải vượt qua C trong nhà máy của bạn và yêu cầu thử nghiệm của bạn xây dựng một MockC trả về MockB, cho phép bạn kiểm tra tất cả các tương tác có thể.

Nếu điều này bắt đầu trở nên phức tạp, đó có thể là một dấu hiệu cho thấy thiết kế của bạn có thể được ghép quá chặt. Nếu bạn có thể nới lỏng khớp nối và giữ độ gắn kết cao thì loại DI này trở nên dễ thực hiện hơn.


Về câu trả lời 2: Bạn có nghĩa là mã sản xuất và mã thử nghiệm sẽ sử dụng hai triển khai nhà máy khác nhau để xây dựng A, trong đó (1) nhà máy sản xuất mã sản xuất sẽ tiêm phiên bản B sản xuất và (2) nhà máy sản xuất mã thử nghiệm sẽ tiêm B ví dụ giả. Trên thực tế, cá thể B được lồng khá sâu trong một cây thành phần. Tôi sẽ phải vượt qua nhà máy thông qua một số cấp độ của cây thành phần. Thể hiện của A được xây dựng bởi đối tượng cha của chúng (một số lớp khác)
Giorgio

Liên quan đến câu hỏi 3, một trong những vấn đề của chúng tôi là làm thế nào nhà máy B nên được đưa vào A: sử dụng đối số hàm tạo trong hàm tạo A để đặt tham chiếu cục bộ đến nhà máy hoặc dưới dạng đơn lẻ mà A truy cập khi cần sử dụng nhà máy .
Giorgio

@Giorgio Hãy xem bản cập nhật của tôi ghi nhận xét của bạn. Không biết ví dụ cụ thể của bạn, các ví dụ chung của tôi có thể không áp dụng, nhưng đây là cách tiếp cận tôi sẽ thực hiện để xem liệu tôi có thể đơn giản hóa vấn đề kiểm tra hay không.
Bringer128

Cảm ơn rất nhiều về ví dụ của bạn (tôi thấy mã giả OK). Hai quan sát: (1) Tại sao nên sử dụng một nhà máy trong mã sản xuất và một hàm tạo đơn giản trong mã thử nghiệm? (2) Hệ thống phân cấp thành phần là C -> D -> A -> B và người dùng C phải cung cấp phiên bản MockB phải được tiêm từ C vào A.
Giorgio

(1) Nhà máy phải ẩn khía cạnh DI khỏi mã sử dụng A. Nó nhằm ngăn chặn việc sử dụng mã trở nên phức tạp hơn khi thêm DI. Nói chính xác hơn, nó cho phép trừu tượng hóa việc quản lý phụ thuộc khỏi A, B, thậm chí C và D. (2) Ví dụ bạn đang đưa ra không thực sự là một bài kiểm tra đơn vị . Nếu bạn chỉ gọi các phương thức trên C, sẽ khó hơn nhiều để có được phạm vi kiểm tra cao trên A. Đối với bạn tầm quan trọng của nó, nhưng kiểm tra đơn vị chỉ nên kiểm tra A, các tương tác của nó với B và các giá trị trả về của nó.
Bringer128
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.