Làm thế nào để bạn cấu trúc các bài kiểm tra đơn vị cho nhiều đối tượng thể hiện cùng một hành vi?


9

Trong rất nhiều trường hợp tôi có thể có một lớp hiện có với một số hành vi:

class Lion
{
    public void Eat(Herbivore herbivore) { ... }
}

... và tôi có một bài kiểm tra đơn vị ...

[TestMethod]
public void Lion_can_eat_herbivore()
{
    var herbivore = buildHerbivoreForEating();
    var test = BuildLionForTest();
    test.Eat(herbivore);
    Assert.IsEaten(herbivore);
}

Bây giờ, những gì xảy ra là tôi cần tạo ra một lớp Tiger với hành vi hợp lý với Sư tử:

class Tiger
{
    public void Eat(Herbivore herbivore) { ... }
}

... Và vì tôi muốn có hành vi tương tự, tôi cần chạy thử nghiệm tương tự, tôi làm một cái gì đó như thế này:

interface IHerbivoreEater
{
    void Eat(Herbivore herbivore);
}

... Và tôi cấu trúc lại bài kiểm tra của mình:

[TestMethod]
public void Lion_can_eat_herbivore()
{
    IHerbivoreEater_can_eat_herbivore(BuildLionForTest);
}


public void IHerbivoreEater_can_eat_herbivore(Func<IHerbivoreEater> builder)
{
    var herbivore = buildHerbivoreForEating();
    var test = builder();
    test.Eat(herbivore);
    Assert.IsEaten(herbivore);
}

... và sau đó tôi thêm một bài kiểm tra khác cho Tigerlớp mới của mình :

[TestMethod]
public void Tiger_can_eat_herbivore()
{
    IHerbivoreEater_can_eat_herbivore(BuildTigerForTest);
}

... và sau đó tôi cấu trúc lại các lớp của tôi LionTiger(thường là do thừa kế, nhưng đôi khi theo thành phần):

class Lion : HerbivoreEater { }
class Tiger : HerbivoreEater { }

abstract class HerbivoreEater : IHerbivoreEater
{
    public void Eat(Herbivore herbivore) { ... }
}

... Và tất cả đều tốt. Tuy nhiên, vì chức năng hiện có trong HerbivoreEaterlớp, nên bây giờ có cảm giác có gì đó không ổn khi có các bài kiểm tra cho từng hành vi này trên mỗi lớp con. Tuy nhiên, đó là các lớp con thực sự đang được tiêu thụ và đó chỉ là một chi tiết triển khai mà chúng xảy ra để chia sẻ các hành vi chồng chéo ( ví dụ Lions, và Tigerscó thể có các mục đích sử dụng hoàn toàn khác nhau).

Có vẻ không cần thiết để kiểm tra cùng một mã nhiều lần, nhưng có những trường hợp lớp con có thể và ghi đè chức năng của lớp cơ sở (vâng, nó có thể vi phạm LSP, nhưng hãy đối mặt với nó, IHerbivoreEaterchỉ là giao diện kiểm tra thuận tiện - nó có thể không quan trọng đối với người dùng cuối). Vì vậy, những thử nghiệm này có một số giá trị, tôi nghĩ.

Những người khác làm gì trong tình huống này? Bạn chỉ di chuyển bài kiểm tra của mình đến lớp cơ sở hay bạn kiểm tra tất cả các lớp con cho hành vi dự kiến?

CHỈNH SỬA :

Dựa trên câu trả lời từ @pdr tôi nghĩ chúng ta nên xem xét điều này: đây IHerbivoreEaterchỉ là một hợp đồng chữ ký phương thức; nó không chỉ định hành vi. Ví dụ:

[TestMethod]
public void Tiger_eats_herbivore_haunches_first()
{
    IHerbivoreEater_eats_herbivore_haunches_first(BuildTigerForTest);
}

[TestMethod]
public void Cheetah_eats_herbivore_haunches_first()
{
    IHerbivoreEater_eats_herbivore_haunches_first(BuildCheetahForTest);
}

[TestMethod]
public void Lion_eats_herbivore_head_first()
{
    IHerbivoreEater_eats_herbivore_head_first(BuildLionForTest);
}

Để tranh luận, bạn không nên có một Animallớp có chứa Eat? Tất cả động vật ăn, và do đó Tiger, Lionlớp và có thể thừa hưởng từ động vật.
Người đàn ông Muffin

1
@Nick - đó là một điểm tốt, nhưng tôi nghĩ đó là một tình huống khác. Như @pdr đã chỉ ra, nếu bạn đặt Eathành vi trong lớp cơ sở, thì tất cả các lớp con sẽ thể hiện Eathành vi tương tự . Tuy nhiên, tôi đang nói về 2 lớp tương đối không liên quan đến nhau để chia sẻ một hành vi. Ví dụ, xem xét Flyhành vi của BrickPersonchúng ta có thể giả sử, thể hiện một hành vi bay tương tự, nhưng không nhất thiết phải có chúng xuất phát từ một lớp cơ sở chung.
Scott Whitlock

Câu trả lời:


6

Điều này thật tuyệt vì nó cho thấy các bài kiểm tra thực sự lái theo cách bạn nghĩ về thiết kế. Bạn đang cảm nhận vấn đề trong thiết kế và đặt câu hỏi đúng.

Có hai cách để xem xét điều này.

IHerbivoreEater là một hợp đồng. Tất cả IHerbivoreEaters phải có phương pháp Eat chấp nhận Động vật ăn cỏ. Bây giờ, các xét nghiệm của bạn không quan tâm nó được ăn như thế nào; Sư tử của bạn có thể bắt đầu bằng những cú nhảy và Tiger có thể bắt đầu từ cổ họng. Tất cả các thử nghiệm của bạn quan tâm là sau khi nó gọi Eat, Herbivore được ăn.

Mặt khác, một phần của những gì bạn đang nói là tất cả các IHerbivoreEaters đều ăn động vật ăn cỏ theo cách chính xác (do đó là lớp cơ sở). Đó là trường hợp, không có điểm nào trong việc có hợp đồng IHerbivoreEater cả. Nó không cung cấp gì. Bạn cũng có thể chỉ thừa kế từ HerbivoreEater.

Hoặc có thể loại bỏ hoàn toàn với Lion và Tiger.

Nhưng, nếu Sư tử và Hổ khác nhau về mọi mặt ngoài thói quen ăn uống của chúng, thì bạn cần bắt đầu tự hỏi liệu bạn có gặp phải vấn đề với cây thừa kế phức tạp không. Điều gì sẽ xảy ra nếu bạn cũng muốn lấy được cả hai lớp từ Feline, hoặc chỉ lớp Lion từ KingOfItsDomain (cùng với Shark, có lẽ). Đây là nơi LSP thực sự đến.

Tôi sẽ đề nghị rằng mã phổ biến được đóng gói tốt hơn.

public class Lion : IHerbivoreEater
{
    private IHerbivoreEatingStrategy _herbivoreEatingStrategy;
    private Lion (IHerbivoreEatingStrategy herbivoreEatingStrategy)
    {
        _herbivoreEatingStrategy = herbivoreEatingStrategy;
    }

    public Lion() : this(new StandardHerbivoreEatingStrategy())
    {
    }

    public void Eat(Herbivore herbivore)
    {
        _herbivoreEatingStrategy.Eat(herbivore);
    }
}

Tiger cũng vậy.

Bây giờ, đây là một thứ đẹp đang phát triển (đẹp vì tôi không có ý định đó). Nếu bạn chỉ cung cấp hàm tạo riêng tư đó cho bài kiểm tra, bạn có thể chuyển vào IHerbivoreEatingStrargety giả và chỉ cần kiểm tra xem tin nhắn được chuyển đến đối tượng được đóng gói đúng cách.

Và bài kiểm tra phức tạp của bạn, bài kiểm tra mà bạn lo lắng ngay từ đầu, chỉ phải kiểm tra StandardHerbivoreEatingStrargety. Một lớp, một bộ kiểm tra, không có mã trùng lặp để lo lắng.

Và nếu, sau này, bạn muốn nói với Hổ rằng chúng nên ăn động vật ăn cỏ của chúng theo một cách khác, không có thử nghiệm nào trong số này phải thay đổi. Bạn chỉ đơn giản là tạo một HerbivoreEatingStrargety mới và thử nghiệm điều đó. Hệ thống dây được thử nghiệm ở cấp độ thử nghiệm tích hợp.


+1 Mô hình chiến lược là điều đầu tiên xuất hiện trong đầu tôi khi đọc câu hỏi.
StuperUser

Rất tốt, nhưng bây giờ thay thế "kiểm tra đơn vị" trong câu hỏi của tôi bằng "kiểm tra tích hợp". Chúng ta không kết thúc với cùng một vấn đề? IHerbivoreEaterlà một hợp đồng, nhưng chỉ trong rất nhiều khi tôi cần nó để thử nghiệm. Dường như với tôi rằng đây là một trường hợp mà việc gõ vịt sẽ thực sự có ích. Bây giờ tôi chỉ muốn gửi cả hai đến cùng một logic kiểm tra. Tôi không nghĩ giao diện sẽ hứa hẹn về hành vi. Các xét nghiệm nên làm điều đó.
Scott Whitlock

câu hỏi tuyệt vời, câu trả lời tuyệt vời Bạn không cần phải có một nhà xây dựng riêng; bạn có thể kết nối IHerbivoreEatingStrargety đến StandardHerbivoreEatingStrargety bằng cách sử dụng bộ chứa IoC.
azheglov

@ScottWhitlock: "Tôi không nghĩ giao diện sẽ hứa hẹn về hành vi. Các bài kiểm tra nên làm điều đó." Đó chính xác là những gì tôi đang nói. Nếu nó thực hiện lời hứa về hành vi, bạn nên loại bỏ nó và chỉ sử dụng lớp (cơ sở). Bạn không cần nó để thử nghiệm cả.
pdr

@azheglov: Đồng ý, nhưng câu trả lời của tôi đã đủ dài rồi :)
pdr

1

Có vẻ không cần thiết để kiểm tra cùng một mã nhiều lần, nhưng có những trường hợp lớp con có thể và ghi đè chức năng của lớp cơ sở

Nói một cách dễ hiểu, bạn có hỏi liệu có phù hợp để sử dụng kiến ​​thức hộp trắng để bỏ qua một số bài kiểm tra hay không. Từ quan điểm hộp đen, LionTigerlà các lớp khác nhau. Vì vậy, một người không quen thuộc với mã sẽ kiểm tra họ, nhưng bạn có kiến ​​thức triển khai sâu sẽ biết rằng bạn có thể thoát khỏi việc chỉ cần thử nghiệm một con vật.

Một phần lý do để phát triển các bài kiểm tra đơn vị là cho phép bản thân bạn cấu trúc lại sau nhưng duy trì giao diện hộp đen tương tự . Các bài kiểm tra đơn vị đang giúp bạn đảm bảo các lớp học của bạn tiếp tục đáp ứng hợp đồng của họ với khách hàng, hoặc ít nhất là buộc bạn phải nhận ra và suy nghĩ cẩn thận về việc hợp đồng có thể thay đổi như thế nào. Bản thân bạn nhận ra rằng Lionhoặc Tigercó thể ghi đè Eatvào một số điểm sau đó. Nếu điều này là có thể từ xa, một thử nghiệm đơn vị thử nghiệm đơn giản mà mỗi động vật bạn hỗ trợ có thể ăn, theo cách:

[TestMethod]
public void Tiger_can_eat_herbivore()
{
    IHerbivoreEater_can_eat_herbivore(BuildTigerForTest);
}

nên rất đơn giản để làm và đủ và sẽ đảm bảo rằng bạn có thể phát hiện khi các đối tượng không đáp ứng hợp đồng của họ.


Tôi tự hỏi nếu câu hỏi này thực sự chỉ tập trung vào việc ưu tiên thử nghiệm hộp đen so với thử nghiệm hộp trắng. Tôi nghiêng về phía trại hộp đen, đó có lẽ là lý do tại sao tôi đang thử nghiệm bản thân mình. Cảm ơn đã chỉ ra rằng.
Scott Whitlock

1

Bạn đang làm đúng. Hãy nghĩ về một bài kiểm tra đơn vị là bài kiểm tra hành vi của một lần sử dụng mã mới của bạn. Đây là cùng một cuộc gọi bạn sẽ thực hiện từ mã sản xuất.

Trong tình huống đó, bạn hoàn toàn chính xác; một người sử dụng Lion hoặc Tiger sẽ không (ít nhất là không phải) quan tâm rằng cả hai đều là HerbivoreEaters và mã thực sự chạy cho phương thức này là phổ biến cho cả hai trong lớp cơ sở. Tương tự, một người sử dụng bản tóm tắt HerbivoreEater (được cung cấp bởi Sư tử hoặc Hổ cụ thể) sẽ không quan tâm đến nó. Điều họ quan tâm là việc Lion, Tiger hoặc triển khai cụ thể của HerbivoreEater sẽ ăn () một động vật ăn cỏ một cách chính xác.

Vì vậy, những gì bạn đang thử nghiệm về cơ bản là Sư tử sẽ ăn như dự định và một con hổ sẽ ăn như dự định. Điều quan trọng là phải kiểm tra cả hai, bởi vì có thể không phải lúc nào cả hai đều ăn chính xác như nhau; bằng cách kiểm tra cả hai, bạn đảm bảo rằng cái bạn không muốn thay đổi, đã không. Bởi vì cả hai đều là HerbivoreEaters được xác định, ít nhất cho đến khi bạn thêm Cheetah, bạn cũng đã kiểm tra rằng tất cả HerbivoreEaters sẽ ăn như dự định. Thử nghiệm của bạn bao gồm hoàn toàn bao gồm và thực hiện đầy đủ mã (với điều kiện là bạn cũng thực hiện tất cả các xác nhận dự kiến ​​về những gì sẽ xảy ra từ HerbivoreEater ăn Herbivore).

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.