Cách tốt nhất để đơn vị phương thức kiểm tra gọi các phương thức khác trong cùng một lớp


35

Gần đây tôi đã thảo luận với một số người bạn về hai phương pháp sau đây là tốt nhất để loại bỏ kết quả trả về hoặc gọi đến các phương thức trong cùng một lớp từ các phương thức trong cùng một lớp.

Đây là một ví dụ rất đơn giản. Trong thực tế các chức năng phức tạp hơn nhiều.

Thí dụ:

public class MyClass
{
     public bool FunctionA()
     {
         return FunctionB() % 2 == 0;
     }

     protected int FunctionB()
     {
         return new Random().Next();
     }
}

Vì vậy, để kiểm tra điều này, chúng tôi có 2 phương pháp.

Phương pháp 1: Sử dụng Hàm và Hành động để thay thế chức năng của các phương thức. Thí dụ:

public class MyClass
{
     public Func<int> FunctionB { get; set; }

     public MyClass()
     {
         FunctionB = FunctionBImpl;
     }

     public bool FunctionA()
     {
         return FunctionB() % 2 == 0;
     }

     protected int FunctionBImpl()
     {
         return new Random().Next();
     }
}

[TestClass]
public class MyClassTests
{
    private MyClass _subject;

    [TestInitialize]
    public void Initialize()
    {
        _subject = new MyClass();
    }

    [TestMethod]
    public void FunctionA_WhenNumberIsOdd_ReturnsTrue()
    {
        _subject.FunctionB = () => 1;

        var result = _subject.FunctionA();

        Assert.IsFalse(result);
    }
}

Phương pháp 2: Làm cho các thành viên ảo, lớp dẫn xuất và trong lớp dẫn xuất sử dụng Hàm và hành động để thay thế chức năng Ví dụ:

public class MyClass
{     
     public bool FunctionA()
     {
         return FunctionB() % 2 == 0;
     }

     protected virtual int FunctionB()
     {
         return new Random().Next();
     }
}

public class TestableMyClass
{
     public Func<int> FunctionBFunc { get; set; }

     public MyClass()
     {
         FunctionBFunc = base.FunctionB;
     }

     protected override int FunctionB()
     {
         return FunctionBFunc();
     }
}

[TestClass]
public class MyClassTests
{
    private TestableMyClass _subject;

    [TestInitialize]
    public void Initialize()
    {
        _subject = new TestableMyClass();
    }

    [TestMethod]
    public void FunctionA_WhenNumberIsOdd_ReturnsTrue()
    {
        _subject.FunctionBFunc = () => 1;

        var result = _subject.FunctionA();

        Assert.IsFalse(result);
    }
}

Tôi muốn biết wich là tốt hơn và TẠI SAO?

Cập nhật: LƯU Ý: FunctionB cũng có thể được công khai


Ví dụ của bạn rất đơn giản, nhưng không chính xác. FunctionAtrả về một bool nhưng chỉ đặt một biến cục bộ xvà không trả về bất cứ thứ gì.
Eric P.

1
Trong ví dụ cụ thể này, FunctionB có thể public staticnhưng trong một lớp khác.

Đối với Đánh giá mã, bạn sẽ đăng mã thực tế không phải là phiên bản đơn giản hóa của mã. Xem Câu hỏi thường gặp. Theo quan điểm của nó, bạn đang hỏi một câu hỏi cụ thể không tìm kiếm một đánh giá mã.
Winston Ewert

1
FunctionBđược thiết kế phá vỡ. new Random().Next()hầu như luôn luôn sai. Bạn nên tiêm ví dụ Random. ( Randomcũng là một lớp được thiết kế tồi, có thể gây ra một vài vấn đề khác)
CodeInChaos

trên một lưu ý chung hơn DI thông qua các đại biểu là hoàn toàn tốt imho
jk.

Câu trả lời:


32

Chỉnh sửa sau khi cập nhật poster gốc.

Tuyên bố miễn trừ trách nhiệm: không phải là lập trình viên C # (chủ yếu là Java hoặc Ruby). Câu trả lời của tôi sẽ là: Tôi sẽ không kiểm tra nó, và tôi không nghĩ bạn nên làm vậy.

Phiên bản dài hơn là: các phương thức riêng tư / được bảo vệ không phải là một phần của API, về cơ bản chúng là các lựa chọn triển khai, mà bạn có thể quyết định xem xét, cập nhật hoặc vứt bỏ hoàn toàn mà không có bất kỳ tác động nào từ bên ngoài.

Tôi cho rằng bạn có một bài kiểm tra về FunctionA (), đây là một phần của lớp có thể nhìn thấy từ thế giới bên ngoài. Nó phải là người duy nhất có hợp đồng để thực hiện (và điều đó có thể được kiểm tra). Phương pháp riêng tư / được bảo vệ của bạn không có hợp đồng để thực hiện và / hoặc thử nghiệm.

Xem một cuộc thảo luận liên quan ở đó: https://stackoverflow.com/questions/105007/should-i-test-private-methods-or-only-public-ones

Theo nhận xét , nếu FunctionB là công khai, tôi sẽ chỉ kiểm tra cả hai bằng cách sử dụng kiểm tra đơn vị. Bạn có thể nghĩ rằng kiểm tra FunctionA không hoàn toàn là "đơn vị" (vì nó gọi là FunctionB), nhưng tôi sẽ không quá lo lắng về điều đó: nếu kiểm tra FunctionB hoạt động nhưng không kiểm tra FunctionA, điều đó có nghĩa rõ ràng là sự cố không nằm trong tên miền phụ của FunctionB, đủ tốt để tôi là người phân biệt đối xử.

Nếu bạn thực sự muốn có thể tách biệt hoàn toàn hai thử nghiệm, tôi sẽ sử dụng một số loại kỹ thuật giả định để chế nhạo FunctionB khi kiểm tra FunctionA (thông thường, trả về một giá trị chính xác đã biết cố định). Tôi thiếu kiến ​​thức về hệ sinh thái C # để tư vấn cho một thư viện chế giễu cụ thể, nhưng bạn có thể xem câu hỏi này .


2
Hoàn toàn đồng ý với câu trả lời @Martin. Khi bạn viết bài kiểm tra đơn vị cho lớp, bạn không nên kiểm tra phương pháp . Những gì bạn đang kiểm tra là một hành vi của lớp , rằng hợp đồng (tuyên bố lớp nào được cho là phải làm) được thỏa mãn. Vì vậy, các bài kiểm tra đơn vị của bạn phải đáp ứng tất cả các yêu cầu được đưa ra cho lớp này (sử dụng các phương thức / thuộc tính công khai), bao gồm các trường hợp đặc biệt

Xin chào, cảm ơn vì đã trả lời, nhưng nó không trả lời câu hỏi của tôi. Tôi không quan trọng nếu FunctionB là riêng tư / được bảo vệ. Nó cũng có thể được công khai và vẫn được gọi từ FunctionA.

Cách phổ biến nhất để xử lý vấn đề này, mà không cần thiết kế lại lớp cơ sở, là phân lớp MyClassvà ghi đè phương thức với chức năng bạn muốn khai thác. Nó cũng có thể là một ý tưởng tốt để cập nhật câu hỏi của bạn để bao gồm FunctionBcó thể công khai.
Eric P.

1
protectedcác phương thức là một phần của bề mặt công khai của một lớp, trừ khi bạn đảm bảo rằng không thể có việc triển khai lớp của bạn trong các hội đồng khác nhau.
CodeInChaos

2
Thực tế là FunctionA gọi FunctionB là một chi tiết không liên quan từ góc độ thử nghiệm đơn vị. Nếu các bài kiểm tra cho FunctionA được viết chính xác, thì đó là một chi tiết triển khai có thể được tái cấu trúc sau đó mà không phá vỡ các bài kiểm tra (miễn là hành vi tổng thể của FunctionA không thay đổi). Vấn đề thực sự là việc truy xuất một số ngẫu nhiên của FunctionB cần phải được thực hiện với một đối tượng được tiêm, để bạn có thể sử dụng một giả trong khi thử nghiệm để đảm bảo rằng một số nổi tiếng được trả về. Điều này cho phép bạn kiểm tra đầu vào / đầu ra nổi tiếng.
Dan Lyons

11

Tôi đăng ký lý thuyết rằng nếu một chức năng là quan trọng để kiểm tra hoặc quan trọng để thay thế, thì điều quan trọng là không phải là một chi tiết thực hiện riêng tư của lớp đang thử nghiệm, mà là một chi tiết thực hiện công khai của một lớp khác .

Vì vậy, nếu tôi ở trong một kịch bản mà tôi có

class A 
{
     public B C()
     {
         D();
     }

     private E D();
     {
         // i actually want to control what this produces when I test C()
         // or this is important enough to test on its own
         // and, typically, both of the above
     }
}

Sau đó tôi sẽ tái cấu trúc.

class A 
{
     ICollaborator collaborator;

     public A(ICollaborator collaborator)
     {
         this.collaborator = collaborator;
     }

     public B C()
     {
         collaborator.D();
     }
}

Bây giờ tôi có một kịch bản trong đó D () có thể kiểm tra độc lập và có thể thay thế hoàn toàn.

Là một phương tiện tổ chức, cộng tác viên của tôi có thể không sống ở cùng cấp độ không gian tên. Ví dụ: nếu Aở trong FooCorp.BLL, thì cộng tác viên của tôi có thể là một lớp sâu khác, như trong FooCorp.BLL.Collaborators (hoặc bất kỳ tên nào là phù hợp). Cộng tác viên của tôi có thể chỉ được nhìn thấy bên trong hội đồng thông qua công cụ internalsửa đổi truy cập, mà sau đó tôi cũng sẽ tiếp xúc với (các) dự án thử nghiệm đơn vị của mình thông qua InternalsVisibleTothuộc tính lắp ráp. Điều đáng nói là bạn vẫn có thể giữ API của mình sạch sẽ, khi có liên quan đến người gọi, trong khi tạo mã có thể kiểm chứng.


Có nếu ICollaborator cần một số phương pháp. Nếu bạn có một đối tượng, công việc duy nhất của họ là bọc một phương thức duy nhất tôi muốn thấy nó được thay thế bằng một đại biểu.
jk.

Bạn sẽ phải quyết định xem một đại biểu có tên có ý nghĩa hay nếu một giao diện có ý nghĩa và tôi sẽ không quyết định cho bạn. Cá nhân, tôi không phản đối các lớp phương thức (công khai). Càng nhỏ càng tốt, vì chúng ngày càng trở nên dễ hiểu.
Anthony Pegram

0

Thêm vào những gì Martin chỉ ra,

Nếu phương pháp của bạn là riêng tư / được bảo vệ - đừng kiểm tra nó. Nó là nội bộ của lớp và không nên được truy cập bên ngoài lớp.

Trong cả hai cách tiếp cận mà bạn đề cập, tôi có những mối quan tâm này -

Phương pháp 1 - Điều này thực sự thay đổi lớp theo hành vi của thử nghiệm trong thử nghiệm.

Phương pháp 2 - Điều này thực sự không kiểm tra mã sản xuất, thay vào đó kiểm tra việc thực hiện khác.

Trong bài toán đã nêu, tôi thấy rằng logic duy nhất của A là xem liệu đầu ra của FunctionB có chẵn không. Mặc dù có tính minh họa, FunctionB cho giá trị Ngẫu nhiên, rất khó để kiểm tra.

Tôi mong đợi một kịch bản thực tế nơi chúng ta có thể thiết lập MyClass để chúng ta biết FunctionB sẽ trả về cái gì. Sau đó, kết quả mong đợi của chúng tôi được biết đến, chúng tôi có thể gọi FunctionA và xác nhận kết quả thực tế.


3
protectedlà gần như giống nhau như public. Chỉ privateinternallà chi tiết thực hiện.
CodeInChaos

@codeinchaos - Tôi tò mò ở đây. Đối với thử nghiệm, các phương thức được bảo vệ là 'riêng tư' trừ khi bạn sửa đổi các thuộc tính lắp ráp. Chỉ các loại dẫn xuất có quyền truy cập vào các thành viên được bảo vệ. Ngoại trừ ảo, tôi không thấy lý do tại sao được bảo vệ nên được đối xử tương tự như công khai từ một thử nghiệm. bạn có thể giải thích?
Srikanth Venugopalan

Vì các lớp dẫn xuất đó có thể nằm trong các hội đồng khác nhau, chúng được tiếp xúc với mã của bên thứ ba và do đó là một phần của bề mặt công khai của lớp của bạn. Để kiểm tra chúng, bạn có thể tạo chúng internal protected, sử dụng một trình trợ giúp phản xạ riêng hoặc tạo một lớp dẫn xuất trong dự án thử nghiệm của bạn.
CodeInChaos

@CodesInChaos, đồng ý rằng lớp dẫn xuất có thể ở các hội đồng khác nhau, nhưng phạm vi vẫn bị giới hạn ở các kiểu cơ sở và dẫn xuất. Sửa đổi công cụ sửa đổi truy cập chỉ để làm cho nó có thể kiểm tra được là điều mà tôi hơi lo lắng. Tôi đã làm điều đó, nhưng nó dường như là một phản hạt đối với tôi.
Srikanth Venugopalan

0

Cá nhân tôi sử dụng Phương thức 1 tức là biến tất cả các phương thức thành Hành động hoặc Func vì điều này đã cải thiện đáng kể khả năng kiểm tra mã cho tôi. Như với bất kỳ giải pháp nào cũng có những ưu và nhược điểm với phương pháp này:

Ưu

  1. Cho phép cấu trúc mã đơn giản trong đó sử dụng Mẫu mã chỉ để Kiểm tra đơn vị có thể tăng thêm độ phức tạp.
  2. Cho phép các lớp được niêm phong và loại bỏ các phương thức ảo được yêu cầu bởi các khung mô phỏng phổ biến như Moq. Các lớp niêm phong và loại bỏ các phương thức ảo làm cho chúng trở thành ứng cử viên cho nội tuyến và tối ưu hóa trình biên dịch khác. ( https://msdn.microsoft.com/en-us/l Library / ff647802.aspx )
  3. Đơn giản hóa khả năng kiểm tra khi thay thế triển khai Func / Action trong kiểm tra Đơn vị cũng đơn giản như chỉ định một giá trị mới cho Func / Action
  4. Cũng cho phép kiểm tra nếu một Func tĩnh được gọi từ một phương thức khác vì các phương thức tĩnh không thể bị chế giễu.
  5. Dễ dàng cấu trúc lại các phương thức hiện có thành Funcs / Action vì cú pháp gọi một phương thức trên trang web cuộc gọi vẫn giữ nguyên. (Trong những lúc bạn không thể cấu trúc lại một phương thức thành Func / Action, hãy xem khuyết điểm)

Nhược điểm

  1. Funcs / Action không thể được sử dụng nếu lớp của bạn có thể được dẫn xuất là Funcs / Action không có đường dẫn kế thừa như Phương thức
  2. Không thể sử dụng các tham số mặc định. Tạo một Func với các tham số mặc định đòi hỏi phải tạo một ủy nhiệm mới có thể làm cho mã khó hiểu tùy theo trường hợp sử dụng
  3. Không thể sử dụng cú pháp tham số được đặt tên để gọi các phương thức như một cái gì đó (FirstName: "S", lastName: "K")
  4. Con lớn nhất là bạn không có quyền truy cập vào tham chiếu 'this' trong Funcs và Action, và vì vậy mọi phụ thuộc vào lớp phải được truyền rõ ràng dưới dạng tham số. Nó tốt như bạn biết tất cả các phụ thuộc nhưng xấu nếu bạn có nhiều thuộc tính mà Func của bạn sẽ phụ thuộc vào. Số dặm của bạn sẽ thay đổi dựa trên trường hợp sử dụng của bạn.

Vì vậy, để tóm tắt, sử dụng Funcs và Action cho bài kiểm tra Đơn vị là tuyệt vời nếu bạn biết rằng các lớp của bạn sẽ không bao giờ bị ghi đè.

Ngoài ra, tôi thường không tạo các thuộc tính cho Func mà trực tiếp tạo các thuộc tính như vậy

public class MyClass
{
     public Func<int> FunctionB = () => new Random().Next();

     public bool FunctionA()
     {
         return FunctionB() % 2 == 0;
     }
}

Hi vọng điêu nay co ich!


-1

Sử dụng Mock của nó có thể. Nuget: https://www.nuget.org/packages/moq/

Và tôi tin rằng nó khá đơn giản và có ý nghĩa.

public class SomeClass
{
    public SomeClass(int a) { }

    public void A()
    {
        B();
    }

    public virtual void B()
    {

    }
}

[TestFixture]
public class Test
{
    [Test]
    public void Test_A_Calls_B()
    {
        var mockedObject = new Mock<SomeClass>(5); // You can also specify constructor arguments.
        //You can also setup what a function can return.
        var obj = mockedObject.Object;
        obj.A();

        Mock.Get(obj).Verify(x=>x.B(),Times.AtLeastOnce);//This test passes
    }
}

Mock cần phương thức ảo để ghi đè.

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.