Các nguyên tắc thiết kế thúc đẩy mã thử nghiệm là gì? (thiết kế mã kiểm tra so với thiết kế lái xe thông qua các bài kiểm tra)


54

Hầu hết các dự án mà tôi làm việc đều xem xét việc phát triển và thử nghiệm đơn vị một cách riêng rẽ, điều này khiến cho việc viết các bài kiểm tra đơn vị sau này trở thành một cơn ác mộng. Mục tiêu của tôi là để giữ cho thử nghiệm trong tâm trí trong giai đoạn thiết kế cấp cao và cấp thấp.

Tôi muốn biết nếu có bất kỳ nguyên tắc thiết kế được xác định rõ ràng nào thúc đẩy mã thử nghiệm. Một nguyên tắc như vậy mà tôi đã hiểu gần đây là Nghịch đảo phụ thuộc thông qua tiêm phụ thuộc và đảo ngược kiểm soát.

Tôi đã đọc được rằng có một cái gì đó gọi là RẮN. Tôi muốn hiểu nếu làm theo các nguyên tắc RẮN gián tiếp dẫn đến mã có thể dễ dàng kiểm tra? Nếu không, có bất kỳ nguyên tắc thiết kế được xác định rõ ràng nào thúc đẩy mã thử nghiệm?

Tôi biết rằng có một thứ gọi là Phát triển hướng thử nghiệm. Mặc dù, tôi quan tâm nhiều hơn đến việc thiết kế mã với kiểm tra trong tâm trí trong giai đoạn thiết kế hơn là điều khiển thiết kế thông qua các thử nghiệm. Tôi hy vọng điều này có ý nghĩa.

Một câu hỏi nữa liên quan đến chủ đề này là liệu có ổn không khi tính lại một sản phẩm / dự án hiện có và thay đổi mã và thiết kế nhằm mục đích có thể viết một trường hợp thử nghiệm đơn vị cho mỗi mô-đun không?



Cảm ơn bạn. Tôi chỉ mới bắt đầu đọc bài báo và nó đã có ý nghĩa.

1
Đây là một trong những câu hỏi phỏng vấn của tôi ("Làm thế nào để bạn thiết kế mã để dễ dàng kiểm tra đơn vị?"). Nó đơn phương chỉ cho tôi nếu họ hiểu thử nghiệm đơn vị, chế giễu / khai thác, OOD và có khả năng TDD. Đáng buồn thay, các câu trả lời thường là một cái gì đó như "Tạo cơ sở dữ liệu thử nghiệm".
Chris Pitman

Câu trả lời:


56

Có, RẮN là một cách rất tốt để thiết kế mã có thể dễ dàng kiểm tra. Là một đoạn mồi ngắn:

S - Nguyên tắc trách nhiệm duy nhất: Một đối tượng nên thực hiện chính xác một việc và phải là đối tượng duy nhất trong cơ sở mã thực hiện một việc đó. Chẳng hạn, lấy một lớp miền, nói Hóa đơn. Lớp Hóa đơn phải thể hiện cấu trúc dữ liệu và quy tắc kinh doanh của hóa đơn như được sử dụng trong hệ thống. Nó phải là lớp duy nhất đại diện cho một hóa đơn trong cơ sở mã. Điều này có thể được chia nhỏ hơn nữa để nói rằng một phương thức nên có một mục đích và phải là phương thức duy nhất trong cơ sở mã đáp ứng nhu cầu này.

Theo nguyên tắc này, bạn tăng khả năng kiểm tra thiết kế của mình bằng cách giảm số lượng kiểm tra bạn phải viết để kiểm tra cùng chức năng trên các đối tượng khác nhau và bạn cũng thường kết thúc bằng các phần chức năng nhỏ hơn để dễ kiểm tra hơn.

O - Nguyên tắc mở / đóng: Một lớp nên được mở để mở rộng, nhưng đóng để thay đổi . Khi một đối tượng tồn tại và hoạt động chính xác, lý tưởng là không cần phải quay lại đối tượng đó để thực hiện các thay đổi có thêm chức năng mới. Thay vào đó, đối tượng nên được mở rộng, bằng cách lấy nó hoặc bằng cách cắm các triển khai phụ thuộc mới hoặc khác nhau vào nó, để cung cấp chức năng mới đó. Điều này tránh hồi quy; bạn có thể giới thiệu chức năng mới khi nào và ở đâu cần thiết, mà không thay đổi hành vi của đối tượng vì nó đã được sử dụng ở nơi khác.

Bằng cách tuân thủ nguyên tắc này, bạn thường tăng khả năng chịu đựng các "giả" của mã và bạn cũng tránh phải viết lại các bài kiểm tra để dự đoán hành vi mới; tất cả các thử nghiệm hiện có cho một đối tượng vẫn sẽ hoạt động trên triển khai chưa được mở rộng, trong khi các thử nghiệm mới cho chức năng mới sử dụng triển khai mở rộng cũng sẽ hoạt động.

Nguyên tắc thay thế L - Liskov: Một lớp A, phụ thuộc vào lớp B, có thể sử dụng bất kỳ X: B nào mà không biết sự khác biệt. Điều này về cơ bản có nghĩa là bất cứ điều gì bạn sử dụng như một phụ thuộc nên có hành vi tương tự như nhìn thấy bởi lớp phụ thuộc. Ví dụ ngắn gọn, giả sử bạn có giao diện IWriter hiển thị Write (chuỗi), được ConsoleWriter triển khai. Bây giờ bạn phải ghi vào một tệp thay thế, vì vậy bạn tạo FileWriter. Khi làm như vậy, bạn phải đảm bảo rằng FileWriter có thể được sử dụng giống như cách ConsoleWriter đã làm (nghĩa là cách duy nhất mà người phụ thuộc có thể tương tác với nó bằng cách gọi Write (chuỗi)), và vì vậy thông tin bổ sung mà FileWriter có thể cần phải làm điều đó công việc (như đường dẫn và tệp để ghi vào) phải được cung cấp từ một nơi khác ngoài người phụ thuộc.

Điều này là rất lớn để viết mã có thể kiểm tra được, bởi vì một thiết kế phù hợp với LSP có thể có một đối tượng "bị chế giễu" thay thế cho vật thật tại bất kỳ thời điểm nào mà không thay đổi hành vi dự kiến, cho phép kiểm tra các đoạn mã nhỏ một cách tự tin hệ thống sau đó sẽ làm việc với các đối tượng thực được cắm vào.

I - Nguyên tắc phân chia giao diện: Một giao diện nên có ít phương thức khả thi để cung cấp chức năng của vai trò được xác định bởi giao diện . Nói một cách đơn giản, nhiều giao diện nhỏ hơn tốt hơn ít giao diện lớn hơn. Điều này là do một giao diện lớn có nhiều lý do để thay đổi và gây ra nhiều thay đổi hơn ở nơi khác trong cơ sở mã có thể không cần thiết.

Việc tuân thủ ISP cải thiện khả năng kiểm tra bằng cách giảm độ phức tạp của các hệ thống được kiểm tra và sự phụ thuộc của các SUT đó. Nếu đối tượng bạn đang kiểm tra phụ thuộc vào giao diện IDoThreeThings hiển thị DoOne (), DoTwo () và DoThree (), bạn phải giả định một đối tượng thực hiện cả ba phương thức ngay cả khi đối tượng chỉ sử dụng phương thức DoTwo. Nhưng, nếu đối tượng chỉ phụ thuộc vào IDoTwo (chỉ hiển thị DoTwo), bạn có thể dễ dàng giả định một đối tượng có một phương thức đó.

D - Nguyên tắc đảo ngược phụ thuộc: Cụ thể và trừu tượng không bao giờ nên phụ thuộc vào các cụ thể khác, mà phụ thuộc vào trừu tượng . Nguyên tắc này trực tiếp thực thi nguyên lý của khớp nối lỏng lẻo. Một đối tượng không bao giờ phải biết IS là gì; thay vào đó nó nên quan tâm những gì một đối tượng LÀM. Vì vậy, việc sử dụng các giao diện và / hoặc các lớp cơ sở trừu tượng luôn được ưu tiên hơn so với việc sử dụng các triển khai cụ thể khi xác định các thuộc tính và tham số của một đối tượng hoặc phương thức. Điều đó cho phép bạn trao đổi một triển khai này cho một triển khai khác mà không phải thay đổi cách sử dụng (nếu bạn cũng tuân theo LSP, song hành với DIP).

Một lần nữa, điều này rất lớn cho khả năng kiểm tra, vì nó cho phép bạn, một lần nữa, đưa một thực thi giả của một phụ thuộc thay vì triển khai "sản xuất" vào đối tượng của bạn đang được thử nghiệm, trong khi vẫn kiểm tra đối tượng ở dạng chính xác mà nó sẽ có trong khi trong sản xuất. Đây là chìa khóa để kiểm tra đơn vị "trong sự cô lập".


16

Tôi đã đọc được rằng có một cái gì đó gọi là RẮN. Tôi muốn hiểu nếu tuân theo các nguyên tắc RẮN gián tiếp dẫn đến mã có thể dễ dàng kiểm tra?

Nếu áp dụng đúng, có. Có bài đăng trên blog của Jeff giải thích các nguyên tắc RẮN theo một cách thực sự ngắn (podcast đã đề cập cũng đáng nghe), tôi khuyên bạn nên xem xét nếu các mô tả dài hơn đang ném bạn đi.

Từ kinh nghiệm của tôi, 2 nguyên tắc từ RẮN đóng vai trò chính trong việc thiết kế mã thử nghiệm:

  • Nguyên tắc phân tách giao diện - bạn nên thích nhiều giao diện dành riêng cho khách hàng thay vì ít giao diện hơn, mục đích chung. Điều này đi đôi với Nguyên tắc Trách nhiệm Đơn lẻ và giúp bạn thiết kế các lớp hướng tính năng / nhiệm vụ, đổi lại sẽ dễ kiểm tra hơn nhiều (so với các khái quát hơn, hoặc thường bị lạm dụng "trình quản lý""bối cảnh" ) - ít phụ thuộc hơn , ít phức tạp hơn, hạt mịn hơn, thử nghiệm rõ ràng. Trong ngắn hạn, các thành phần nhỏ dẫn đến các bài kiểm tra đơn giản.
  • Nguyên tắc đảo ngược phụ thuộc - thiết kế theo hợp đồng, không phải bằng cách thực hiện. Điều này sẽ có lợi cho bạn nhất khi kiểm tra các đối tượng phức tạp và nhận ra bạn không cần toàn bộ biểu đồ phụ thuộc chỉ để thiết lập nó , nhưng bạn có thể chỉ cần giả lập giao diện và thực hiện với nó.

Tôi tin rằng hai điều này sẽ hỗ trợ bạn nhiều nhất khi thiết kế để kiểm tra. Những cái còn lại cũng có tác động, nhưng tôi muốn nói là không lớn.

(...) liệu có ổn định lại hệ số của một sản phẩm / dự án hiện có và thay đổi mã và thiết kế nhằm mục đích có thể viết trường hợp thử nghiệm đơn vị cho mỗi mô-đun không?

Không có kiểm tra đơn vị hiện có, nó chỉ đơn giản là đặt - yêu cầu rắc rối. Kiểm tra đơn vị là đảm bảo rằng mã của bạn hoạt động . Giới thiệu thay đổi vi phạm được phát hiện ngay lập tức nếu bạn có phạm vi kiểm tra phù hợp.

Bây giờ, nếu bạn muốn thay đổi mã hiện có để thêm các bài kiểm tra đơn vị , thì điều này đưa ra một khoảng trống nơi bạn chưa có bài kiểm tra, nhưng đã thay đổi mã rồi . Đương nhiên, bạn có thể không biết những thay đổi của mình đã phá vỡ. Đây là tình huống bạn muốn tránh.

Các bài kiểm tra đơn vị đáng để viết bằng mọi cách, thậm chí chống lại mã khó kiểm tra. Nếu mã của bạn đang hoạt động , nhưng không được thử nghiệm đơn vị, giải pháp thích hợp sẽ là viết thử nghiệm cho nó và sau đó đưa ra các thay đổi. Tuy nhiên, lưu ý rằng việc thay đổi mã được kiểm tra để làm cho nó dễ kiểm tra hơn là điều mà quản lý của bạn có thể không muốn chi tiền (có thể bạn sẽ nghe thấy nó mang lại ít giá trị kinh doanh).


iaw sự gắn kết cao và khớp nối thấp
jk.

8

CÂU HỎI ĐẦU TIÊN CỦA BẠN:

RẮN thực sự là con đường để đi. Tôi thấy rằng hai khía cạnh quan trọng nhất của từ viết tắt RẮN, khi nói đến khả năng kiểm tra, là S (Trách nhiệm duy nhất) và D (Tiêm phụ thuộc).

Trách nhiệm duy nhất : Các lớp học của bạn thực sự chỉ nên làm một việc, và một điều duy nhất. một lớp tạo ra một tệp, phân tích một số đầu vào và ghi nó vào tệp đã thực hiện ba điều. Nếu lớp học của bạn chỉ làm một việc, bạn sẽ biết chính xác những gì mong đợi về nó và việc thiết kế các trường hợp thử nghiệm cho việc đó khá dễ dàng.

Dependency Injection (DI): Điều này cho phép bạn kiểm soát môi trường thử nghiệm. Thay vì tạo các đối tượng forreign bên trong mã của bạn, bạn tiêm nó thông qua hàm tạo của lớp hoặc gọi phương thức. Khi không quan tâm, bạn chỉ cần thay thế các lớp thực sự bằng các sơ khai hoặc giả, mà bạn kiểm soát hoàn toàn.

CÂU HỎI THỨ HAI CỦA BẠN: Lý tưởng nhất là bạn viết các bài kiểm tra chứng minh chức năng của mã của bạn trước khi tái cấu trúc nó. Bằng cách này, bạn có thể ghi lại rằng tái cấu trúc của bạn tái tạo các kết quả giống như mã gốc. Tuy nhiên, vấn đề của bạn là mã chức năng rất khó kiểm tra. Đây là một tình huống cổ điển! Lời khuyên của tôi là: Hãy suy nghĩ cẩn thận về tái cấu trúc trước khi thử nghiệm đơn vị. Nếu bạn có thể; viết các bài kiểm tra cho mã làm việc, sau đó cấu trúc lại mã và sau đó cấu trúc lại các bài kiểm tra. Tôi biết nó sẽ tốn hàng giờ, nhưng bạn sẽ chắc chắn hơn, rằng mã được cấu trúc lại làm giống như cũ. Phải nói rằng, tôi đã từ bỏ rất nhiều lần. Các lớp học có thể xấu và lộn xộn đến mức viết lại là cách duy nhất để làm cho chúng có thể kiểm tra được.


4

Ngoài các câu trả lời khác, tập trung vào việc đạt được khớp nối lỏng lẻo, tôi muốn nói một từ về việc kiểm tra logic phức tạp.

Tôi đã từng phải kiểm tra đơn vị một lớp có logic phức tạp, với rất nhiều điều kiện và khó hiểu được vai trò của các trường.

Tôi đã thay thế mã này bằng nhiều lớp nhỏ đại diện cho một máy trạng thái . Logic trở nên đơn giản hơn nhiều để làm theo, vì các trạng thái khác nhau của lớp trước trở nên rõ ràng. Mỗi lớp trạng thái là độc lập với các lớp khác, và vì vậy chúng có thể dễ dàng kiểm tra.

Thực tế là các trạng thái rõ ràng giúp dễ dàng liệt kê tất cả các đường dẫn có thể có của mã (chuyển trạng thái), và do đó để viết một bài kiểm tra đơn vị cho mỗi một.

Tất nhiên, không phải mọi logic phức tạp đều có thể được mô hình hóa như một máy trạng thái.


3

RẮN là một khởi đầu tuyệt vời, theo kinh nghiệm của tôi, bốn trong số các khía cạnh của RẮN thực sự hoạt động tốt với thử nghiệm đơn vị.

  • Nguyên tắc trách nhiệm duy nhất - mỗi lớp chỉ làm một việc và một việc duy nhất. Tính toán một giá trị, mở một tệp, phân tích chuỗi, bất cứ điều gì. Do đó, số lượng đầu vào và đầu ra, cũng như các điểm quyết định nên rất nhỏ. Điều này làm cho nó dễ dàng để viết bài kiểm tra.
  • Nguyên tắc thay thế Liskov - bạn sẽ có thể thay thế trong sơ khai và giả mà không thay đổi các thuộc tính mong muốn (kết quả mong đợi) của mã của bạn.
  • Nguyên tắc phân tách giao diện - tách các điểm tiếp xúc bằng các giao diện giúp dễ dàng sử dụng khung mô phỏng như Moq để tạo sơ khai và giả. Thay vì phải dựa vào các lớp cụ thể, bạn chỉ đơn giản dựa vào một cái gì đó thực hiện giao diện.
  • Nguyên tắc tiêm phụ thuộc - Đây là những gì cho phép bạn tiêm các cuống và giả đó vào mã của mình thông qua một hàm tạo, thuộc tính hoặc tham số trong phương thức bạn muốn kiểm tra.

Tôi cũng sẽ xem xét các mẫu khác nhau, đặc biệt là mẫu nhà máy. Giả sử bạn có một lớp cụ thể thực hiện giao diện. Bạn sẽ tạo một nhà máy để khởi tạo lớp cụ thể, nhưng thay vào đó trả lại giao diện.

public interface ISomeInterface
{
    int GetValue();
}  

public class SomeClass : ISomeInterface
{
    public int GetValue()
    {
         return 1;
    }
}

public interface ISomeOtherInterface
{
    bool IsSuccess();
}

public class SomeOtherClass : ISomeOtherInterface
{
     private ISomeInterface m_SomeInterface;

     public SomeOtherClass(ISomeInterface someInterface)
     {
          m_SomeInterface = someInterface;
     }

     public bool IsSuccess()
     {
          return m_SomeInterface.GetValue() == 1;
     }
}

public class SomeFactory
{
     public virtual ISomeInterface GetSomeInterface()
     {
          return new SomeClass();
     }

     public virtual ISomeOtherInterface GetSomeOtherInterface()
     {
          ISomeInterface someInterface = GetSomeInterface();

          return new SomeOtherClass(someInterface);
     }
}

Trong các thử nghiệm của bạn, bạn có thể Moq hoặc một số khung mô phỏng khác để ghi đè phương thức ảo đó và trả về giao diện của thiết kế. Nhưng theo như mã thực hiện thì nhà máy không thay đổi. Bạn cũng có thể ẩn rất nhiều chi tiết triển khai theo cách này, mã triển khai của bạn không quan tâm đến cách giao diện được xây dựng, tất cả những gì nó quan tâm là lấy lại giao diện.

Nếu bạn muốn mở rộng về điều này một chút, tôi khuyên bạn nên đọc Nghệ thuật kiểm tra đơn vị . Nó đưa ra một số ví dụ tuyệt vời về cách sử dụng các nguyên tắc này, và nó là một cách đọc khá nhanh.


1
Nó được gọi là nguyên tắc "đảo ngược" phụ thuộc, không phải là nguyên tắc "tiêm".
Mathias Lykkegaard Lorenzen
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.