Thử nghiệm đơn vị, nhà máy, và Luật Demeter


8

Đây là cách mã của tôi hoạt động. Tôi có một đối tượng đại diện cho trạng thái hiện tại của một thứ gì đó giống với đơn đặt hàng trong giỏ hàng, được lưu trữ trong API mua sắm của bên thứ 3. Trong mã điều khiển của tôi, tôi muốn có thể gọi:

myOrder.updateQuantity(2);

Để thực sự gửi tin nhắn cho bên thứ ba, bên thứ ba cũng cần biết một số điều cụ thể theo thứ tự NÀY, như orderID, và loginID, sẽ không thay đổi trong vòng đời của ứng dụng.

Vì vậy, khi tôi tạo myOrderban đầu, tôi tiêm một MessageFactory, mà biết loginID. Sau đó, khi updateQuantityđược gọi, Orderđi qua orderID. Mã kiểm soát rất dễ viết. Một luồng khác xử lý cuộc gọi lại và cập nhật Ordernếu thay đổi của nó thành công hoặc thông báo Orderrằng thay đổi của nó không thành công nếu không.

Vấn đề là thử nghiệm. Bởi vì Orderđối tượng phụ thuộc vào a MessageFactoryvà nó cần MessageFactorytrả về Messages thực tế ( .setOrderID()ví dụ như nó gọi ), nên bây giờ tôi phải thiết lập các giả rất phức tạp MessageFactory. Ngoài ra, tôi không muốn giết bất kỳ nàng tiên nào, vì "Mỗi khi một Mock trả lại một Mock, một nàng tiên chết."

Làm thế nào tôi có thể giải quyết vấn đề này trong khi giữ mã điều khiển đơn giản như vậy? Tôi đã đọc câu hỏi này: /programming/791940/law-of-demeter-on-factory-potype-and-dependency-injection nhưng nó không giúp ích gì vì nó không nói về vấn đề kiểm tra .

Một vài giải pháp tôi đã nghĩ đến:

  1. Bằng cách nào đó, cấu trúc lại mã để không yêu cầu phương thức nhà máy trả về các đối tượng thực. Có lẽ nó ít hơn một nhà máy và nhiều hơn một MessageSender?
  2. Tạo một triển khai chỉ thử nghiệm MessageFactoryvà tiêm nó.

Mã này có liên quan khá nhiều, đây là nỗ lực của tôi tại một sscce:

public class Order implements UpdateHandler {
    private final MessageFactory factory;
    private final MessageLayer layer;

    private OrderData data;

    // Package private constructor, this should only be called by the OrderBuilder object.
    Order(OrderBuilder builder, OrderData initial) {
        this.factory = builder.getFactory();
        this.layer = builder.getLayer();
        this.data = original;
    }

    // Lots of methods like this
    public String getItemID() {
        return data.getItemID();
    }

    // Returns true if the message was placed in the outgoing network queue successfully. Doesn't block for receipt, though.
    public boolean updateQuantity(int newQuantity) {
        Message newMessage = factory.createOrderModification(messageInfo);

        // *** THIS IS THE KEY LINE ***
        // throws an NPE if factory is a mock.
        newMessage.setQuantity(newQuantity); 

        return layer.send(newMessage); 
    }

    // from interface UpdateHandler
    // gets called asynchronously
    @Override 
    public handleUpdate(OrderUpdate update) {
        messageInfo.handleUpdate(update);
    }
}

Tôi nghĩ rằng bạn sẽ phải cho chúng tôi xem mã có liên quan mà bạn đã viết cho Orderđối tượng và MessageFactory. Đây là một mô tả tốt, nhưng nó hơi trừu tượng để giải quyết trực tiếp với một câu trả lời rõ ràng.
Robert Harvey

@RobertHarvey Tôi hy vọng rằng bản cập nhật sẽ giúp.
durron597

Bạn đang cố gắng xác minh rằng layer.sendgửi tin nhắn, hoặc nó gửi tin nhắn chính xác ?
Robert Harvey

@RobertHarvey Tôi đã suy nghĩ về việc gọi điện verify(messageMock).setQuantity(2), và verify(layer).send(messageMock);cũng updateQuantitynên trả về false nếu Orderbản cập nhật đang chờ xử lý, nhưng tôi đã bỏ qua mã đó vì lý do sscce.
durron597

3
Tôi sẽ phải xem mã của bạn một số nữa ... Nhưng đối với hồ sơ, tôi không xem việc trả lại một bản giả là một vấn đề lớn. Sự phức tạp của thử nghiệm dựa trên giả là hậu quả không thể tránh khỏi khi đối phó với các đối tượng có thể thay đổi và vì một giả có liên quan đến ứng dụng được phát hành cuối cùng của bạn, tôi không thấy cách trả lại một con mèo con, ít tiên nữ hơn.
Robert Harvey

Câu trả lời:


12

Mối quan tâm chính ở đây là giả không thể (hoặc không nên) trả lại giả. Đây có lẽ là lời khuyên tốt, nhưng nói về một giải pháp: trả lại một thực tế Message. Nếu Messagelớp học được kiểm tra tốt và vượt qua, bạn có thể coi nó chỉ thân thiện như một bản giả. Có lẽ nó thậm chí còn thân thiện hơn vì nó sẽ phản hồi như thật vì nó là thật.

Những loại thực sự Messagebạn có thể trở lại? Chà, bạn có thể trả về một thực tế đầy đủ Message, một thực tế đơn giản hóa Message(trong đó các mặc định nổi tiếng được sử dụng) hoặc bạn có thể trả về một NullMessage(như trong Mẫu đối tượng Null). A NullMessagecũng hợp lệ Messagenhư bất kỳ cái nào khác và có thể được bỏ ở bất kỳ nơi nào khác trong ứng dụng của bạn. Cái nào sẽ sử dụng phụ thuộc vào sự phức tạp của việc tạo và trả lại một thông điệp đầy đủ.

Đối với Luật Demeter, có nhiều mối quan tâm ở đây. Đầu tiên, hàm tạo của bạn lấy trình xây dựng riêng của nó làm tham số, sau đó trích xuất các phần tử từ nó. Đây là một sự vi phạm rõ ràng của Demeter, và cũng tạo ra sự phụ thuộc không cần thiết. Tệ hơn nữa, người xây dựng đang hoạt động như một công cụ định vị dịch vụ nhỏ, che giấu sự phụ thuộc thực sự của lớp. Các OrderBuildernên tạo các đối tượng này và vượt qua chúng trong khi các thông số riêng của họ.

Sau đó, để kiểm tra điều này, bạn sẽ chuyển qua một bản giả MessageFactory, trả về một số thực Message(đầy đủ, đơn giản hoặc null) và một bản giả MessageLayernhận thông điệp. Nếu bạn sử dụng đầy đủ hoặc đơn giản hóa Message, bạn có thể lấy lại từ bản MessageLayergiả của mình và kiểm tra nó để kiểm tra các xác nhận.

Tôi cũng sẽ xem xét MessageFactoryMessageLayernhư là một cụm chức năng ở một mức độ trừu tượng khác nhau, và vì vậy tôi sẽ trích xuất một MessageSenderlớp đóng gói chức năng đó. Bạn có thể kiểm tra lớp này bằng cách sử dụng một bản giả đơn giản MessageSendervà chuyển mọi thứ tôi đã nói ở trên sang các MessageSenderbài kiểm tra, từ đó cũng tuân thủ chặt chẽ hơn với Trách nhiệm duy nhất.


Tôi thấy thực sự có hai câu hỏi ở đây. Có một câu hỏi cụ thể về cách kiểm tra mã này một câu hỏi chung về giả lập trả lại giả. Câu hỏi cụ thể là những gì tôi đã giải quyết ở trên với mức độ lớn hơn và tôi có nhiều suy nghĩ hơn ở cuối về vấn đề này khi một số chi tiết khác được đưa ra ánh sáng, nhưng thực sự chưa có câu trả lời hay cho câu hỏi chung: Tại sao giả không nên trả lại giả?

Lý do giả không nên trả lại giả là bạn có thể kết thúc kiểm tra các bài kiểm tra của mình thay vì kiểm tra mã của bạn. Thay vì chỉ đảm bảo rằng thiết bị có đầy đủ chức năng, thử nghiệm bây giờ phụ thuộc vào một đoạn mã hoàn toàn mới chỉ được tìm thấy trong chính trường hợp thử nghiệm (thường không được thử nghiệm). Điều này tạo ra hai vấn đề.

Đầu tiên, thử nghiệm bây giờ không thể cho tôi biết chắc chắn nếu thiết bị bị hỏng hoặc nếu các giả có liên quan bị hỏng. Toàn bộ quan điểm của một bài kiểm tra là tạo ra một môi trường biệt lập, nơi chỉ có một nguyên nhân gây ra thất bại. Bản thân một bản giả thường rất đơn giản và có thể được kiểm tra trực tiếp cho các vấn đề, nhưng việc kết nối nhiều giả với nhau như thế này trở nên khó khăn hơn để xác nhận bằng cách kiểm tra.

Vấn đề thứ hai là, khi các API thay đổi đối với các đối tượng thực, các thử nghiệm có thể bắt đầu thất bại do các giả thiết cũng không tự động thay đổi. Luật Demeter ra đời ở đây, vì đây chính xác là loại hiệu ứng tuân theo luật tránh. Trong các thử nghiệm của mình, tôi sẽ phải lo lắng về việc giữ đồng bộ không chỉ các bản nhái của các phụ thuộc trực tiếp, mà cả các bản phụ thuộc của các phụ thuộc của quảng cáo phụ thuộc . Điều này có tác dụng của phẫu thuật shotgun trong các bài kiểm tra khi các lớp thay đổi.


Bây giờ, đối với câu hỏi cụ thể về cách kiểm tra đoạn mã cụ thể này, chúng ta hãy chia nhỏ một số giả định.

Câu hỏi 1: Chúng ta thực sự đang thử nghiệm điều gì? Mặc dù đây là một phần viết tắt của mã, chúng ta có thể thấy ba hoạt động thiết yếu đang diễn ra ở đây. Đầu tiên, chúng tôi có một nhà máy tạo ra một Message. Chúng tôi không kiểm tra xem nhà máy có sản xuất hay không Message, vì bạn đã chế giễu điều đó. Chúng tôi sẽ không kiểm tra Message, vì nó nên được thử nghiệm ở nơi khác, có lẽ là trong một bộ thử nghiệm cho API của bên thứ ba tạo ra Message. Trong dòng thứ hai, chúng ta có thể thấy từ kiểm tra rằng phương thức này được gọi đơn giản trên Messagevà vì vậy thực sự không có gì để kiểm tra trong dòng thứ hai. Một lần nữa, cần có các thử nghiệm ở những nơi khác giúp thử nghiệm dự phòng này. Dòng thứ ba gọi MessageLayerAPI và chỉ cần chuyển qua kết quả. Một lần nữa,MessageLayerAPI của đã được thử nghiệm ở nơi khác. Điều này khiến chúng ta không có gì để kiểm tra. Không có tác dụng phụ có thể nhìn thấy trực tiếp đối với mã bên ngoài và chúng ta không nên thử nghiệm triển khai nội bộ. Điều đó dẫn chúng ta đến một kết luận rằng sẽ không phù hợp để kiểm tra mã này . (Để biết thêm về dòng lý luận này, hãy xem bài thuyết trình Magic Tricks of Test của Sandi Metz , [ slide , video ])

Câu hỏi 2: Đợi đã, vậy thì ... sao ?? Vâng, đúng vậy, đừng kiểm tra điều này cả. Bây giờ, như đã đề cập, đây là một phiên bản rút gọn của mã. Nếu bạn có logic khác, hãy kiểm tra điều đó, nhưng gói gọn nó thành một đơn vị riêng (như cách MessageSenderthực hiện được đề cập ở trên). Sau đó, bạn có thể giả định toàn bộ khía cạnh của mã này một cách dễ dàng, trong khi vẫn có khả năng kiểm tra logic khác.

Về cơ bản, bạn đang sử dụng API của bên thứ ba trực tiếp trong mã của mình. Mã của bên thứ ba nổi tiếng là khó kiểm tra vì nó có thể có các loại vấn đề phụ thuộc mà bạn có ở đây. Đóng gói nó vào một khu vực bị uốn cong có thể giúp kiểm tra mã khác của bạn dễ dàng hơn và giảm phẫu thuật shotgun nếu bên thứ ba đó thay đổi mã của họ (hoặc chỉ thay đổi). Mặc dù vẫn có thể gặp khó khăn khi thử nghiệm phần tương tác với API của bên thứ ba, nhưng nó bị giới hạn ở một khía cạnh nhỏ mà bạn có thể cô lập.


+1 Hoàn toàn đồng ý về các nhà xây dựng. Không chắc chắn rằng việc trả lại NullMessage sẽ tốt hơn trả lại Mock - có vẻ như bị trượt bởi tính kỹ thuật. Dù bằng cách nào thì kết quả là "Giả".
Rob

Tôi đã đánh cắp thủ thuật "lấy người xây dựng riêng làm đối số" từ nguồn của Guava.
durron597

@RobY Một đối tượng null không phải là giả mạo nếu bạn sử dụng nó như một phần của ứng dụng của bạn như là một hợp pháp không có op. Tuy nhiên, nếu nó thực sự chỉ được sử dụng như một thử nghiệm giả, thì Messagenên sử dụng một loại thực tế khác .
cbojar

@cbojar Được cấp, nhưng Tùy chọn / Có thể đang nổi lên như một cách tốt hơn để xử lý các tài liệu tham khảo vô giá trị. Nếu bạn chỉ viết một NullObject để kiểm tra đơn vị, thì NullObject thực sự chỉ là một bản giả viết tay, giống như ngày xưa. Vấn đề là, nếu không có sự biện minh rõ ràng cho quy tắc "không giả từ giả", thật khó để nói liệu NullObject có tệ hay không, hoặc tại sao , ngoại trừ việc nó có thể hoặc không phá vỡ quy tắc phong cách chủ quan.
Rob

BTW, việc Messagetriển khai thực sự duy nhất phụ thuộc vào API của bên thứ ba; để có thể tạo các thể hiện của các lớp của họ, bạn cần bắt đầu các lớp Engineyêu cầu, trong số những thứ khác, khóa cấp phép ... không chính xác như bạn muốn trong bài kiểm tra đơn vị.
durron597

2

Tôi sẽ đồng ý với @Robert Harvey. Chỉ cần rõ ràng: Demeter là hợp lý và phong cách lập trình tốt. Đó là "không có giả từ giả" gây ấn tượng với tôi vì nhiều sở thích chủ quan hỗ trợ một phong cách mã hóa cụ thể hơn là một thực tiễn thường được áp dụng (& cũng hợp lý). Quy tắc cổ tích đưa ra các giao diện "trôi chảy" như:

Thing.create ("zoom"). SetDomain ("bo.com"). Add (1) .flip (). Reverse (). TuneForSemantics (). Run ();

Một loại ví dụ cực đoan, nhưng về cơ bản, quy tắc cổ tích sẽ không cho phép bao gồm lớp đó trong bất kỳ mã nào, bởi vì mã sẽ trở nên không thể kiểm chứng được. Nhưng đó là một mô hình phổ biến trong mã OO.

Ngoài ra, vấn đề chung hơn là làm thế nào để giả một nhà máy để trả lại một giả mà bạn muốn thử nghiệm. Tôi thường ngại sử dụng Factories làm phụ thuộc, nhưng đôi khi nó tốt hơn nhiều so với giải pháp thay thế. Nếu bạn kết thúc với

Thứ ba ThirdPartyFactory<ThirdPartyThing>#create()

Tôi không thấy làm thế nào bạn có thể đi xung quanh nó. Bạn cần một mock để trả lại mock. Vì vậy, loại quy tắc đó đánh bật hai mẫu thiết kế OO thực sự mạnh mẽ.

Tôi không thể nghĩ ra cách khắc phục vấn đề của bạn mà không chia phương thức đó thành 2 hoặc 3, đẩy các hàng dài lên máy khách hoặc nếu không sẽ tạo ra một lớp bao bọc trạng thái kỳ lạ.

Tôi thực sự quan tâm để xem những gì thay thế trông như thế nào.

Câu trả lời của tôi: mã của bạn là tốt! Excelcior!

(thực sự tôi tò mò về các lựa chọn thay thế)

...

Chúng ta hãy lấy một trường hợp ranh giới: các giả có được phép quay lại không? Về mặt kỹ thuật, họ đang trả lại một bản giả. Nếu không, thì điều đó đánh bật nguyên mẫu GoF và đó là một trong những mô hình đã tồn tại theo thời gian:

MuActor prototype = ...
...
MuActor actor = prototype.create();
actor.run();

quy tắc cũng cho phép:

prototype = Mock(MuActor.class);
when(prototype.create()).thenReturn(prototype);

Ngoài ra, quy tắc cổ tích khá nhiều cấm sử dụng Monads, bởi vì chúng dựa trên chuỗi hoạt động cho một loại container cụ thể. Bạn có thể kiểm tra loại Monad, nhưng bạn không thể kiểm tra mã trong đó Monad xuất hiện.


Giao diện thông thạo vẫn có thể theo Demeter vì giao diện thông thạo trả về đối tượng tự giống nhau. Nói cách khác, obj.law().of().demeter();giống như obj.law(); obj.of(); obj.demeter();, điều này là hoàn toàn chấp nhận được. Một lời giải thích tương tự có thể được thực hiện về nguyên mẫu.
cbojar

Có, nhưng nếu họ thực sự làm việc và bạn muốn chế nhạo họ, thì bạn sẽ chống lại quy tắc "không giả từ giả". Điều đó có nghĩa là mã sử dụng chúng trở nên không thể kiểm chứng.
Rob

@cbojar oh, và tôi nên rõ ràng. Demeter nghe có vẻ hợp lý, và một phong cách lập trình tốt. Đó là "không có giả từ giả" gây ấn tượng với tôi vì nhiều sở thích chủ quan hỗ trợ một phong cách mã hóa cụ thể hơn là một thực tiễn thường được áp dụng (& cũng hợp lý).
Rob

Nhân tiện, trong một phần khác của mã tôi sử dụng giao diện trôi chảy. Tôi có một số ý tưởng từ bài đăng trên blog này và đây là mã thực tế tôi sử dụng: pastebin.com/D1GxSPsy
durron597

Thật khéo léo. Mát mẻ! Tôi sẽ thử nó trong mã tôi đang làm việc bây giờ. Chỉ cần một lưu ý - một giao diện trôi chảy không cần phải tự quay lại. Có biến thể trong đó giao diện lưu loát trả về một loạt các trường hợp bất biến. tức là thay vì "return this", nó sử dụng "return new Fluent (...)". Tuy nhiên, điều đó minh bạch với khách hàng, vì vậy nó không ảnh hưởng đến bất kỳ cuộc thảo luận nào ở đây. Nhưng đó là một sự thật thú vị.
Rob
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.