Có thể giả mạo một phần của lớp đang thử nghiệm?


22

Giả sử tôi có một lớp (tha thứ cho ví dụ giả định và thiết kế xấu của nó):

class MyProfit
{
  public decimal GetNewYorkRevenue();
  public decimal GetNewYorkExpenses();
  public decimal GetNewYorkProfit();

  public decimal GetMiamiRevenue();
  public decimal GetMiamiExpenses();
  public decimal GetMiamiProfit();

  public bool BothCitiesProfitable();

}

(Lưu ý các phương thức GetxxxRevenue () và GetxxxExpenses () có các phụ thuộc được loại bỏ)

Bây giờ tôi đang kiểm tra đơn vị TwoCitiesProfitable () phụ thuộc vào GetNewYorkProfit () và GetMiamiProfit (). Có ổn không khi khai thác GetNewYorkProfit () và GetMiamiProfit ()?

Có vẻ như nếu tôi không thì tôi đồng thời thử nghiệm GetNewYorkProfit () và GetMiamiProfit () cùng với cả TwoCitiesProfitable (). Tôi phải đảm bảo rằng tôi thiết lập sơ khai cho GetxxxRevenue () và GetxxxExpenses () để các phương thức GetxxxProfit () trả về các giá trị chính xác.

Cho đến nay tôi chỉ thấy ví dụ về việc phụ thuộc vào các lớp bên ngoài chứ không phải các phương thức bên trong.

Và nếu nó ổn, có một mô hình cụ thể nào tôi nên sử dụng để làm điều này?

CẬP NHẬT

Tôi lo ngại rằng chúng ta có thể thiếu vấn đề cốt lõi và đó có lẽ là lỗi của ví dụ đáng thương của tôi. Câu hỏi cơ bản là: nếu một phương thức trong một lớp có sự phụ thuộc vào một phương thức được phơi bày công khai khác trong cùng một lớp đó thì có ổn không (hoặc thậm chí được đề xuất) để loại bỏ phương thức khác đó?

Có thể tôi đang thiếu một cái gì đó, nhưng tôi không chắc việc chia lớp luôn có ý nghĩa. Có lẽ một ví dụ tốt hơn tối thiểu sẽ là:

class Person
{
 public string FirstName()
 public string LastName()
 public string FullName()
}

trong đó tên đầy đủ được định nghĩa là:

public string FullName()
{
  return FirstName() + " " + LastName();
}

Có thể bỏ sơ khai FirstName () và LastName () khi kiểm tra FullName () không?


Nếu bạn kiểm tra một số mã đồng thời, làm thế nào điều này có thể xấu? Có vấn đề gì vậy? Thông thường, tốt hơn là nên kiểm tra mã thường xuyên hơn và chuyên sâu hơn.
người dùng không xác định

Chỉ cần tìm hiểu về bản cập nhật của bạn, tôi đã cập nhật câu trả lời của mình
Winston Ewert

Câu trả lời:


27

Bạn nên chia tay lớp học theo câu hỏi.

Mỗi lớp nên làm một số nhiệm vụ đơn giản. Nếu nhiệm vụ của bạn quá phức tạp để kiểm tra, thì nhiệm vụ mà lớp thực hiện là quá lớn.

Bỏ qua sự ngu ngốc của thiết kế này:

class NewYork
{
    decimal GetRevenue();
    decimal GetExpenses();
    decimal GetProfit();
}


class Miami
{
    decimal GetRevenue();
    decimal GetExpenses();
    decimal GetProfit();
}

class MyProfit
{
     MyProfit(NewYork new_york, Miami miami);
     boolean bothProfitable();
}

CẬP NHẬT

Vấn đề với các phương thức khai thác trong một lớp là bạn đang vi phạm việc đóng gói. Thử nghiệm của bạn nên được kiểm tra để xem liệu hành vi bên ngoài của đối tượng có khớp với thông số kỹ thuật hay không. Bất cứ điều gì xảy ra bên trong đối tượng không phải là việc của nó.

Thực tế là FullName sử dụng FirstName và LastName là một chi tiết triển khai. Không có gì ngoài lớp nên quan tâm đó là sự thật. Bằng cách chế nhạo các phương thức công khai để kiểm tra đối tượng, bạn đang đưa ra một giả định về đối tượng đó được thực hiện.

Tại một số điểm trong tương lai, giả định đó có thể chấm dứt là chính xác. Có lẽ tất cả logic tên sẽ được chuyển đến một đối tượng Tên mà Người chỉ đơn giản gọi. Có lẽ FullName sẽ truy cập trực tiếp các biến thành viên First_name và last_name sau đó gọi FirstName và LastName.

Câu hỏi thứ hai là tại sao bạn cảm thấy cần phải làm như vậy. Sau khi tất cả các lớp người của bạn có thể được kiểm tra một cái gì đó như:

Person person = new Person("John", "Doe");
Test.AssertEquals(person.FullName(), "John Doe");

Bạn không nên cảm thấy cần thiết bất cứ điều gì cho ví dụ này. Nếu bạn làm thế thì bạn vẫn còn hạnh phúc và ... dừng lại đi! Không có lợi ích gì khi chế nhạo các phương thức ở đó bởi vì dù sao bạn cũng đã kiểm soát được những gì trong đó.

Trường hợp duy nhất có vẻ hợp lý đối với các phương thức mà FullName sử dụng bị chế giễu là nếu bằng cách nào đó FirstName () và LastName () là các hoạt động không tầm thường. Có thể bạn đang viết một trong những trình tạo tên ngẫu nhiên đó hoặc FirstName và LastName truy vấn cơ sở dữ liệu để trả lời hoặc một cái gì đó. Nhưng nếu đó là những gì đang xảy ra thì nó gợi ý rằng đối tượng đang làm điều gì đó không thuộc về lớp Người.

Đặt nó theo một cách khác, chế nhạo các phương thức là lấy đối tượng và chia thành hai mảnh. Một mảnh đang bị chế giễu trong khi mảnh kia đang được thử nghiệm. Những gì bạn đang làm về cơ bản là một sự phá vỡ đặc biệt của đối tượng. Nếu đó là trường hợp, chỉ cần phá vỡ các đối tượng.

Nếu lớp học của bạn đơn giản, bạn không nên cảm thấy cần phải chế giễu các phần của nó trong bài kiểm tra. Nếu lớp học của bạn đủ phức tạp để bạn cảm thấy cần phải chế giễu, thì bạn nên chia lớp thành những phần đơn giản hơn.

CẬP NHẬT

Theo cách tôi nhìn thấy, một đối tượng có hành vi bên ngoài và bên trong. Hành vi bên ngoài bao gồm trả về các giá trị cuộc gọi vào các đối tượng khác, v.v ... Rõ ràng, bất cứ điều gì trong danh mục đó nên được kiểm tra. (nếu không bạn sẽ kiểm tra cái gì?) Nhưng hành vi nội bộ không thực sự được kiểm tra.

Bây giờ hành vi nội bộ được kiểm tra, bởi vì đó là những gì dẫn đến hành vi bên ngoài. Nhưng tôi không viết các bài kiểm tra trực tiếp về hành vi bên trong, chỉ gián tiếp thông qua hành vi bên ngoài.

Nếu tôi muốn kiểm tra một cái gì đó, tôi nghĩ rằng nó nên được di chuyển để nó trở thành hành vi bên ngoài. Đó là lý do tại sao tôi nghĩ rằng nếu bạn muốn chế giễu một cái gì đó, bạn nên chia nhỏ đối tượng để điều bạn muốn chế giễu bây giờ nằm ​​trong hành vi bên ngoài của các đối tượng được đề cập.

Nhưng, nó có gì khác biệt? Nếu FirstName () và LastName () là thành viên của một đối tượng khác thì nó có thực sự thay đổi vấn đề của FullName () không? Nếu chúng ta quyết định rằng nó không cần thiết để chế nhạo FirstName và LastName có thực sự giúp họ ở trên một đối tượng khác không?

Tôi nghĩ rằng nếu bạn sử dụng phương pháp chế giễu của mình, thì bạn sẽ tạo ra một đường may trong đối tượng. Bạn có các hàm như FirstName () và LastName () giao tiếp trực tiếp với nguồn dữ liệu ngoài. Bạn cũng có FullName () mà không có. Nhưng vì tất cả họ đều ở trong cùng một lớp không rõ ràng. Một số phần không được phép truy cập trực tiếp vào nguồn dữ liệu và các phần khác thì không. Mã của bạn sẽ rõ ràng hơn nếu chỉ cần tách ra khỏi hai nhóm đó.

CHỈNH SỬA

Hãy lùi lại một bước và hỏi: tại sao chúng ta chế nhạo các đối tượng khi chúng ta kiểm tra?

  1. Làm cho các bài kiểm tra chạy ổn định (tránh truy cập vào những thứ thay đổi từ chạy sang chạy)
  2. Tránh truy cập các tài nguyên đắt tiền (không đánh các dịch vụ của bên thứ ba, v.v.)
  3. Đơn giản hóa hệ thống đang thử nghiệm
  4. Giúp dễ dàng kiểm tra tất cả các tình huống có thể xảy ra (ví dụ: những thứ như mô phỏng thất bại, v.v.)
  5. Tránh phụ thuộc vào chi tiết của các đoạn mã khác để thay đổi trong các đoạn mã khác đó sẽ không phá vỡ bài kiểm tra này.

Bây giờ, tôi nghĩ lý do 1-4 không áp dụng cho kịch bản này. Việc nhạo báng nguồn bên ngoài khi kiểm tra fullname sẽ xử lý tất cả những lý do đó để chế nhạo. Phần duy nhất không được xử lý đó là sự đơn giản, nhưng có vẻ như đối tượng đủ đơn giản không phải là vấn đề đáng lo ngại.

Tôi nghĩ rằng mối quan tâm của bạn là lý do số 5. ​​Mối quan tâm là tại một thời điểm nào đó trong tương lai thay đổi việc triển khai FirstName và LastName sẽ phá vỡ bài kiểm tra. Trong tương lai FirstName và LastName có thể lấy tên từ một vị trí hoặc nguồn khác nhau. Nhưng FullName có thể sẽ luôn luôn như vậy FirstName() + " " + LastName(). Đó là lý do tại sao bạn muốn kiểm tra FullName bằng cách chế nhạo FirstName và LastName.

Những gì bạn có sau đó, là một số tập hợp con của đối tượng người có nhiều khả năng thay đổi sau đó những người khác. Phần còn lại của đối tượng sử dụng tập hợp con này. Tập hợp con đó hiện đang tìm nạp dữ liệu của nó bằng một nguồn, nhưng có thể tìm nạp dữ liệu đó theo một cách hoàn toàn khác vào một ngày sau đó. Nhưng với tôi có vẻ như tập hợp con đó là một đối tượng riêng biệt đang cố gắng thoát ra.

Dường như với tôi rằng nếu bạn chế giễu phương thức của đối tượng thì bạn đang chia tách đối tượng. Nhưng bạn đang làm như vậy một cách đặc biệt. Mã của bạn không làm rõ rằng có hai phần riêng biệt bên trong đối tượng Person của bạn. Vì vậy, chỉ cần phân tách đối tượng đó trong mã thực tế của bạn, để nó rõ ràng khỏi việc đọc mã của bạn những gì đang xảy ra. Chọn phân chia thực tế của đối tượng có ý nghĩa và không cố gắng phân tách đối tượng khác nhau cho mỗi thử nghiệm.

Tôi nghi ngờ bạn có thể phản đối việc chia tách đối tượng của bạn, nhưng tại sao?

CHỈNH SỬA

Tôi đã sai.

Bạn nên phân tách các đối tượng thay vì giới thiệu các phân tách đặc biệt bằng cách chế nhạo các phương thức riêng lẻ. Tuy nhiên, tôi đã quá tập trung vào một phương pháp chia tách các đối tượng. Tuy nhiên, OO cung cấp nhiều phương pháp phân tách một đối tượng.

Những gì tôi đề xuất:

class PersonBase
{
      abstract sring FirstName();
      abstract string LastName();

      string FullName()
      {
            return FirstName() + " " + LastName();
      }
 }

 class Person extends PersonBase
 {
      string FirstName(); 
      string LastName();
 }

 class FakePerson extends PersonBase
 {
      void setFirstName(string);
      void setLastName(string);
      string getFirstName();
      string getLastName();
 }

Có lẽ đó là những gì bạn đã làm tất cả cùng. Nhưng tôi không nghĩ rằng phương pháp này sẽ có những vấn đề tôi thấy với các phương thức chế giễu bởi vì chúng tôi đã phân định rõ ràng từng phương pháp được áp dụng. Và bằng cách sử dụng tính kế thừa, chúng tôi tránh được sự lúng túng sẽ phát sinh nếu chúng tôi sử dụng một đối tượng trình bao bọc bổ sung.

Điều này không giới thiệu một số phức tạp và chỉ với một vài chức năng tiện ích, có lẽ tôi chỉ cần kiểm tra chúng bằng cách chế nhạo nguồn bên thứ 3 bên dưới. Chắc chắn, chúng có nguy cơ bị phá vỡ nhưng nó không đáng để sắp xếp lại. Nếu bạn có một đối tượng đủ phức tạp mà bạn cần phải phân tách nó, thì tôi nghĩ một cái gì đó như thế này là một ý tưởng tốt.


8
Nói hay lắm. Nếu bạn đang cố gắng tìm cách giải quyết trong thử nghiệm, có lẽ bạn cần xem xét lại thiết kế của mình (như một lưu ý phụ, bây giờ tôi có giọng nói của Frank Sinatra bị mắc kẹt trong đầu).
Có vấn đề

2
"Bằng cách chế nhạo các phương thức công khai để kiểm tra đối tượng, bạn đang đưa ra một giả định về [cách thức] đối tượng đó được thực hiện." Nhưng đó không phải là trường hợp bất cứ khi nào bạn khai thác một đối tượng? Ví dụ: giả sử tôi biết phương thức xyz () của tôi gọi một số đối tượng khác. Để kiểm tra xyz () trong sự cô lập, tôi phải khai thác đối tượng khác. Do đó, thử nghiệm của tôi phải biết về các chi tiết triển khai của phương thức xyz () của tôi.
Người dùng

Trong trường hợp của tôi, các phương thức "FirstName ()" và "LastName ()" rất đơn giản nhưng chúng truy vấn api của bên thứ ba về kết quả của chúng.
Người dùng

@ Người dùng, đã cập nhật, chắc chắn bạn đang chế nhạo api của bên thứ 3 để kiểm tra FirstName và LastName. Có gì sai khi thực hiện cùng một chế độ giả khi kiểm tra FullName?
Winston Ewert

@Winston, đó là toàn bộ quan điểm của tôi, hiện tại tôi chế giễu api của bên thứ 3 được sử dụng trong tên và họ để kiểm tra tên đầy đủ (), nhưng tôi không quan tâm đến cách triển khai tên và họ khi thử nghiệm tên đầy đủ (tất nhiên Không sao khi tôi kiểm tra tên và họ). Do đó câu hỏi của tôi về chế nhạo tên và họ.
Người dùng

10

Mặc dù tôi đồng ý với câu trả lời của Winston Ewert, đôi khi không thể chia tay một lớp, cho dù do hạn chế về thời gian, API đã được sử dụng ở nơi khác hoặc bạn có gì.

Trong trường hợp như vậy, thay vì các phương thức chế giễu, tôi sẽ viết các bài kiểm tra của mình để bao quát lớp tăng dần. Kiểm tra các phương thức getExpenses()getRevenue(), sau đó kiểm tra các getProfit()phương thức, sau đó kiểm tra các phương thức được chia sẻ. Đúng là bạn sẽ có nhiều hơn một thử nghiệm bao gồm một phương pháp cụ thể, nhưng vì bạn đã viết các thử nghiệm để bao gồm các phương thức riêng lẻ, bạn có thể chắc chắn rằng các đầu ra của bạn đáng tin cậy với đầu vào được thử nghiệm.


1
Đã đồng ý. Nhưng chỉ cần nhấn mạnh điểm cho bất kỳ ai đọc nó, đây là giải pháp bạn sử dụng nếu bạn không thể làm giải pháp tốt hơn.
Winston Ewert

5
@Winston, và đây là giải pháp bạn sử dụng trước khi có giải pháp tốt hơn. Tức là có một quả bóng lớn của mã kế thừa, bạn phải che nó bằng các bài kiểm tra đơn vị trước khi bạn có thể cấu trúc lại nó.
Péter Török

@ Péter Török, điểm tốt. Mặc dù tôi nghĩ rằng điều đó được đề cập trong phần "không thể làm giải pháp tốt hơn." :)
Winston Ewert

@all Việc kiểm tra phương thức getProfit () sẽ rất khó khăn vì các kịch bản khác nhau cho getExpenses () và getRevenues () sẽ thực sự nhân các kịch bản cần thiết cho getProfit (). nếu chúng tôi đang kiểm tra getExpenses () và nhận Doanh thu () một cách độc lập Có phải là một ý tưởng tốt để chế giễu các phương thức này để thử nghiệm getProfit () không?
Mohd Farid

7

Để đơn giản hóa các ví dụ của bạn hơn nữa, giả sử bạn đang thử nghiệm C(), phụ thuộc vào A()B(), mỗi trong số đó có phụ thuộc riêng. IMO nó thuộc về những gì bài kiểm tra của bạn đang cố gắng đạt được.

Nếu bạn đang kiểm tra hành vi của những hành vi đã C()biết A()B()sau đó có lẽ là dễ nhất và tốt nhất để loại bỏ A()B(). Điều này có thể sẽ được gọi là một bài kiểm tra đơn vị bởi những người theo chủ nghĩa thuần túy.

Nếu bạn đang kiểm tra hành vi của toàn bộ hệ thống ( C()theo quan điểm của mình), tôi sẽ rời khỏi A()B()như đã thực hiện và loại bỏ các phụ thuộc của chúng (nếu có thể kiểm tra) hoặc thiết lập môi trường hộp cát, chẳng hạn như kiểm tra cơ sở dữ liệu. Tôi sẽ gọi đây là một bài kiểm tra tích hợp.

Cả hai cách tiếp cận đều có thể hợp lệ, vì vậy nếu nó được thiết kế để bạn có thể kiểm tra nó theo cách trước thì có thể sẽ tốt hơn trong thời gian dài.

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.