The art of Unit Testing Chapter 3

Phần 2 - Các kĩ thuật chính Tôi sẽ thiệu về core testing và refactoring techniques cần thiết cho việc viết tests trong thực tế. Trong chương 3, chúng ta sẽ bắt đầu examining stubs và chúng sẽ giúp break dependencies. Chúng ta sẽ xem các kĩ thuật refactoring giúp code dễ test, và bạn sẽ học về seams in the process Trong chương 4, chúng ta sẽ học về mock objects và interaction testing và xem mock object hoạt động khác với stubs, và chúng ta sẽ khám phá các khái niệm về fakes. Trong chương 5, chúng ta sẽ học về isolations frameworks (cũng được biết là mocking framework) và chúng giải quyết với mocks và stubs. Chương 6 cũng so sánh các leading isolation frameworks trong .NET và sử dụng FakeItEasy cho ví dụ, đưa ra các API trong các trường hợp thường gặp. Chương 3: Sử dụng stubs để break dependencies.

  • Trong chương này sẽ bao gồm:
  • Định nghĩa stubs.
  • Refactoring code để sử dụng stubs.
  • Vượt qua tính đóng gói trong code.
  • Khám phá best practices trong sử dụng stubs

Trong các chương trước, bạn đã viết unittesst đầu tiên sử dụng NUnit và khám phá một vài test attributes. Bạn cũng đã built xây dựng test cho các use cases, nơi mà bạn phải kiểm tra giá trị trả về từ đối tượng hoặc state of unit under test trong brea-bones system. Trong chương này, chúng ta sẽ xem các ví dụ thực tế hơn, Nơi mà đối object under test dựa vào các đối tượng khác, bạn không điều khiển được (hoặc nó chưa làm việc). Các object này có thể là một web service, time of day, threading, hoặc nhiều thứ khác. Điểm quan trọng là các test của bạn không thể điều khiển được các kết quả trả về của cá dependencies này trong coder under test hoặc các hành vi của chúng (nếu bạn muốn mô phỏng một exception). Thì đó là lúc bạn sử dụng stubs. 3.1. Giới thiệu về stubs. Đưa người vào vũ trụ là một thách thức thú vị đối với các kĩ sư và các phi hành gia, một trong những khó khăn là làm thế nào để chắc chắn các phi hành gia sẵn sàng cho việc vào không gian và vận hành tất các các máy móc trong quỹ đạo. Một full integration test trong vũ trụ, đòi hỏi phải có vũ trụ, và hiển nhiên là không an toàn để test các phi hành gia. Tại sao NASA có thể xây dựng một full simulators.

DEFINITION An external dependency is an object in your system that your code under test interacts with and over which you have no control. (Common examples are filesystems, threads, memory, time, and so on.)

Việc điều khiển external dependency trong code của bạn là một chủ đề trong chương này, và hầu hết trong cuốn sách này, sẽ dealing with it. Trong lập trình, bạn sử dụng stubs xung quanh vấn đề về external dependencies.

DEFINITION A stub is a controllable replacement for an existing dependency (or collaborator) in the system. By using a stub, you can test your code without dealing with the dependency directly.

Trong chương 4 chúng ta sẽ có một định nghĩa rộng hơn về stubs, mocks và fakes, và chúng liên quan tới những cái khác như thế nào. Còn bây giờ, thứ quan trọng cần nhớ là về mocks với stubs. bạn có thể assert against mock object nhưng bạn không thể assert against a stubs (không hiểu assert against là gì). Hãy cùng xem một ví dụ và cùng làm một thứ gì đó phức tạp hơn cho LogAnalyzer class được giới thiệu trong chương trước. Chúng ta sẽ cố gắng để gỡ rối các dependency trong file system. Test pattern names: xUnit Test Patterns: Refactoring Test Code bởi Gerard Meszators (Addison Wesley, 2007) là một sách cơ bản cho unit testing. Nó định nghĩa các mẫu cho bạn fake trong test của bạn theo ít nhất 5 cách, cái mà tôi cảm thấy gây bối rối cho mọi người (mặc dù nó rất chi tiết). Trong cuốn sách này, tôi sử dụng chỉ 3 định nghĩa cho fake things trong test: fakes, stubs, và mocks. Tôi cảm thấy rằng đươn giản hóa điều này giúp dễ dàng đọc, hiểu các mẫu và không cần phải biết nhiều hơn 3 thứ để bắt đầu viết một tests tốt. Trong nhiều phần của cuốn sách, tôi sẽ refer tới mẫu sử dụng trong xUnit Test Patterns vì thế bạn có thể dễ dàng refer tới Meszaros’s định nghĩa nều bạn thích. 3.2. Xác định một file system dependency trong LogAn LogAnalyzaer class có thể cấu hình để handle nhiều log filename extension sử dụng một special adapter cho mỗi file. Để cho đơn giản, giả sử rằng tên file được lưu trữ vài nơi trên đĩa như một cấu hình cài đặt cho ứng dụng, và IsValidLogFileName như sau:

public bool IsValidLogFileName(string fileName)
{
//read through the configuration file
//return true if configuration says extension is supported.
}

Vấn đề phát sinh, như hình 3,1 rằng mỗi test phụ thuộc vào filesystem, bạn đang performing một integration test, và bạn có các vấn đề liên quan: integration tests chạy chậm, chúng ta cần cấu hình, chúng test nhiều thứ và vì thế. Đây là cơ bản của test-inhibiting design: code có vài phụ thuộc trên một nguồn ngoài, cái mà có thể break test mặc dù thông qua code logic thì oki. Trong legacy system, một single unit of work (action trong hệ thống) phải có nhiều phụ thuộc trên external resources thông qua cái mà test của bạn là nhỏ, nếu bấy kỳ, điều khiển. Tong chương 10 sẽ động chạm tới nhiều chủ đề của legacy code. 3.3. Xác định xem test LogAnalyzer dễ dàng như thế nào “There is no object-oriented problem that cannot be solved by adding a layer of indirection, except, of course, too many layers of indirection.” Không có vấn đề hướng đối tượng nào không thể được giải quyết bằng cách thêm các lớp gián tiếp, trừ khi, có quá nhiều lớp gián tiếp. Tôi thích quote này bởi vì không quá nhiều nghệ thuật trong nghệ thuật testing như về cách tìm đúng nơi để thêm hoặc sử dụng một lớp gián tiếp để test code base. Bạn không thể test thứ gì đó? Thêm một lớp wrap up gọi tới thứ đó, và sau đó thì bắt trước lớp đó trong test của bạn, Hoặc làm ra thứ gì đó thay thế. … The art also involves figuring out when a layer of indirection already exists instead of having to invent it or knowing when not to use it because it complicates things too much. But let’s take it one step at a time. Bạn có thể viết test cho đoạn code này, có một file cấu hình trong filesystem. Bởi vì bạn đang cố tránh các dependencies, bạn muốn code của bạn dễ dàng để test mà không cần nhờ cậy tới integration testing. Nếu bạn nhìn thấy sự giống nhau của các phi hành gia chúng ta bắt đầu, bạn có thể nhìn thấy có 3 định nghĩa về breaking the dependency.

  1. Tìm inteface hoặc API của object under test làm việc lại. Trong trường hợp phi hành gia, điều này là cần điều khiển và màn hình trong phi thuyền
  2. Thay thế các underlying implementtaion của interface với vài thứ khác bạn điều khiển nó. Điều này như là hooking up tới nhiều màn hình và cần điều khiển, các nút trong phòng nơi mà kĩ sử test có thể điểu khiển được tàu không gian interface (khó hiểu quá)

Chuyển mẫu trong code của bạn yêu cầu thêm các bổ qua bỏ qua Về đã Đối tượng thay thế của bạn sẽ không nói chuyện với filesystem, nó sẽ break dependency trên filesystem. Bởi vì bạn không testing class nói chuyện với filesystem nhưng code gọi class này, Nó là OK nếu stub class không làm bất cứ thứ gì nhưng lại là những tín hiệu tốt khi chạy trong test. Tôi đã thêm một interface vào mĩ. Inteface này sẽ cho phép đối tượng model theo cách abstract hệ thống bởi FileExtensionManager class làm và nó sẽ cho phép test tạo một stub giống như FileExtensionManager. Bạn sẽ nhìn thấy nhiều hơn phương thức này trong phần tiếp theo. Chúng ta sẽ xem một cách giới thiệu cách làm cho code có thể test, bằng cách tạo ra một interface mới. Bây giờ để ý tưởng refactoring và giới thiệu seams into your code. 3.4. Refactoring your design để dễ dàng test. Đây là thời điểm để giới thiệu hai khái niệm mới được sử dụng trong suốt cuốn sách là refactoring và seams.

DEFINITION Refactoring is the act of changing code without changing the code’s functionality. That is, it does exactly the same job as it did before. No more and no less. It just looks different. A refactoring example might be renaming a method and breaking a long method into several smaller methods.

Refactoring là hành động thay đổi code mà không thay đổi chức năng của code. Nó vẫn hoạt động chính xác như trước đây. Không nhiều hơn và không ít hơn. Nó chỉ trông khác đi. Một ví dụ refactoring có thể là đổi tên phương thức và breaking một phương thức quá dài thành nhiều phương thức nhỏ hơn.

DEFINITION Seams are places in your code where you can plug in different functionality, such as stub classes, adding a constructor parameter, adding a public settable property, making a method virtual so it can be overridden, or externalizing a delegate as a parameter or property so that it can be set from outside a class. Seams are what you get by implementing the Open-Closed Principle, where a class’s functionality is open for extenuation, but its source code is closed for direct modification. (See Working Effectively with Legacy Code by Michael Feathers, for more about seams, or Clean Code by Robert Martin about the Open-Closed Principle.)

Seams là phần code mà nơi mà bạn có thể gắn thêm các chức năng khác, như là stub classes, thêm một constructor parameter, thêm public settable property, tạo nên một method virtual vì thế nó có thể overridden, hoặc externalizing a delegate as a parameter hoặc property vì thế nó có thể được set từ ngoài class. Seams là cái mà bạn get bằng cách implementing the Open-Closed Principle, nơi mà chức năng của class là mở cho việc mở rộng như mã code của nó là đóng cho việc sửa đổi. (See Working Effectively with Legacy Code bởi Michael Feathers, nhiều hơn về seams, hoặc Clean Code bởi Robert Martin về Open-Closed Principle). Bạn có thể refactor code bằng cách introducing một seam mới vào nó mà không thay đổi chức năng chính của code. Vậy chính xác là gì. Tôi đã làm gì để introducing new IExtensionManager interface. Và refactor bạn sẽ. Trước khi bạn làm nó, Tôi sẽ nhắc bạn rằng refactoring code mà không có bất kì having any sort of automated tests against it (integration or otherwise) có thể sẽ là sự kết thúc sự nghiệp nếu bạn không cẩn thận. Luôn luôn có vài integration test xem xét sau lưng bạn trước khi bạn thay đổi code của bạn, hoặc ít nhấn là có một dự định nghỉ ngơi - 1 bản copy của code trước khi bạn bắt đầu refactoring, hi vọng rằng bạn có source control, với một nice, visible comment “before starting refactoring” rằng bạn có thể dễ dàng tìm thấy sau đó. Trong chương này, tôi giả sử rằng bạn có thể có vài integration tests rồi và bạn chạy chúng sau mỗi refactoring để xem xem code vẫn pass. Nhưng chúng ta không tập trung vào chúng bởi vì cuốn sách này là về unit testing. Để break the dependency giữa code của bạn dưới test và filesystem, bạn có thể introduce một hoặc nhiều seams vào code của bạn. Bạn chỉ cần chắc chắn rằng kết quả code chính xác thứ nó đã làm trước kia. Có 2 kiểu của dependency breaking refactorings, và một cái phụ thuộc vào cái khác. Tôi gọi chúng là Type A và Type B refactorings:

  • Type A - Abstracting concrete object vào interface hoặc delegates
  • Type B - Refactoring cho phép injection của fake implementation vào delegate hoặc interface.

Trong danh sách dưới đây chỉ có item đầu tiên là loại A refactoring.

  • Type A - Extract một interface để cho phép thay đổi underlying implementation
  • Type B - Inject stub implementation vào một class under test
  • Type B - Inject một fake vào một constructor level
  • Type B - Inject một fake như là property get hoặc set
  • Type B - Inject một fake trước khi gọi method

Chúng ta sẽ xem xét từng cái bên dưới 3.4.1. Extract một inteface để cho phép underlying implementation Trong kĩ thuật này bạn cần break out code để sử dụng filesystem thành một separate class. Cách này bạn có thể dễ dàng tách nó ra và sau đó thay gọi đó thành class bạn test.

public bool IsValidLogFileName(string fileName)
{ FileExtensionManager mgr =
new FileExtensionManager();
return mgr.IsValid(fileName);
} class FileExtensionManager
{
public bool IsValid(string fileName)
{
//read some file here
}
}