Features supporting future-proofing and usability

Một phần của tài liệu Manning the art of unit testing with examples in c sharp 2nd (Trang 141 - 144)

A future-proof test will fail only for the right reasons in the face of big changes to the production code in the future. Usability is the quality that allows you to easily under- stand and use the framework. Isolation frameworks can be very easy to use badly and cause very fragile and less-future-proof tests.

115 Features supporting future-proofing and usability

These are some features that promote test robustness:

■ Recursive fakes

■ Defaulting to ignored arguments on behaviors and verifications

■ Nonstrict verifications and behavior

■ Wide-area faking 6.3.1 Recursive fakes

Recursive faking is a special behavior of fake objects in the case where functions return other objects. Those objects will always be fake, automatically. Any objects returned by functions in those automatically faked objects will be fake as well, recursively.

Here’s an example:

public interface IPerson {

IPerson GetManager();

} [Test]

public void RecursiveFakes_work() {

IPerson p = Substitute.For<IPerson>();

Assert.IsNotNull(p.GetManager());

Assert.IsNotNull(p.GetManager().GetManager());

Assert.IsNotNull(p.GetManager().GetManager().GetManager());

}

Notice how you don’t need to do anything except write a single line of code to get this working. But why is this ability important? The less you have to tell the test setup about each specific API needing to be fake, the less coupled your test is to the actual imple- mentation of production code, and the less you need to change the test if production code changes in the future.

Not all isolation frameworks allow recursive fakes, so check for this ability on your favorite framework. As far as I know, only .NET frameworks currently even consider this ability. I wish this existed in other languages as well.

Also note that constrained frameworks in .NET can only support recursive fakes on those functions that can be overridden by generated code: public methods that are virtual or part of an interface.

Some people are afraid that such a feature will more easily allow for the breaking of the law of Demeter (http://en.wikipedia.org/wiki/Law_of_Demeter). I disagree, because good design isn’t enforced by a tool but is created by people talking to and teaching each other and by doing code reviews as pairs. But you’ll see more on the topic of design in chapter 11.

6.3.2 Ignored arguments by default

Currently, in all frameworks except Typemock Isolator, any argument values you send into behavior-changing APIs or verification APIs are used as the default expected values.

116 CHAPTER 6 Digging deeper into isolation frameworks

Isolator, by default, ignores values you send in, unless you specifically say in the API calls that you care about the argument values. There’s no need to always include Arg.IsAny<Type> in all methods, which saves typing and avoids generics that hinder readability. With Typemock Isolator (typemock.com), to throw an exception whatever the arguments are, you can just write this:

Isolate.WhenCalled(() => stubLogger.Write(""))

.WillThrow(new Exception("Fake"));

6.3.3 Wide faking

Wide faking is the ability to fake multiple methods at once. In a way, recursive fakes are a subfeature of that idea, but there are also other implementations.

With tools like FakeItEasy, for example, you can signify that all methods of a cer- tain object will return the same value, or just the methods that return a specific type:

A.CallTo(foo).Throws(new Exception());

A.CallTo(foo).WithReturnType<string>().Returns("hello world");

With Typemock, you can signify that all static methods of a type will return a fake value by default:

Isolate.Fake.StaticMethods(typeof(HttpRuntime));

From this moment on, each static method on that object returns a fake value based on its type or a recursively fake object if it returns an object.

Again, I think this is great for the future sustainability of the tests as the production code evolves. A method that’s added and used by production code six months from now will be automatically faked by all existing tests, so that those tests don’t care about the new method.

6.3.4 Nonstrict behavior of fakes

The world of isolation frameworks used to be a very strict one and mostly still is. Many of the frameworks in languages other than .NET (such as Java and Ruby) are by default strict, whereas many of the .NET frameworks had grown out of that stage.

A strict fake’s methods can only be invoked successfully if you set them as

“expected” by the isolation API. This ability to expect that a method on a fake object will be called doesn’t exist in NSubstitute (or FakeItEasy), but it does exist in many of the other frameworks in .NET and other languages (see Moq, Rhino Mocks, and the Typemock Isolator’s old API).

If a method were configured to be expected, then any call that differs from the expectation (for example, I expect method LogError to be called with a parameter of a at the beginning of the test), either by the parameter values defined or by the method name, will usually be handled by throwing an exception.

The test will usually fail on the first unexpected method call to a strict mock object.

I say usually because whether the mock throws an exception depends on the imple- mentation of the isolation framework. Some frameworks allow you to define whether to delay all exceptions until calling verify() at the end of the test.

117 Isolation framework design antipatterns

The main reasons many frameworks were designed this way can be found in the book Growing Object-Oriented Software, Guided by Tests by Freeman and Pryce (Addison- Wesley Professional, 2009). In that book, they use the mock assertions to describe the

“protocol” of communication between objects. Because a protocol is something that needs to be quite strict, reading the test should help you understand the way an object expects to be interacted with.

So what’s problematic about this? The idea itself is not a problem; it’s the ease with which one can abuse and overuse this ability that’s the problem.

A strict mock can fail in two ways: when an unexpected method is called on it, or when expected methods aren’t called on it (which is determined by calling Received()).

It’s the former that bothers me. Assuming I don’t care about internal protocols between objects that are internal to my unit of work, I shouldn’t assert on their inter- actions, or I would be in a world of hurt. A test can fail if I decide to call a method on some internal object in the unit of work that’s unrelated to the end result of that unit of work. Nevertheless, my test will fail, whining, “You didn’t tell me someone will call that method!”

6.3.5 Nonstrict mocks

Most of the time, nonstrict mocks make for less-brittle tests. A nonstrict mock object will allow any call to be made to it, even if it wasn’t expected. For methods that return values, it will return the default value if it’s a value object or null for an object. In more advanced frameworks, there’s also the notion of recursive fakes, in which a fake object that has a method that returns an object will return a fake object by default from that method. And that fake object will also return fake objects from its methods that return objects, recursively. (This exists in Typemock Isolator, as well as NSub, Moq, and par- tially in Rhino Mocks.)

Listing 5.3 in chapter 5 is a pure example of nonstrict mocks. You don’t care what other calls happened. Listing 5.4 and the code block after it show how you can make the test more robust and future-proof by using an argument matcher instead of expecting a full string. Argument matching allows you to create rules on how parame- ters should be passed for the fake to consider them OK. Notice how it uglies up the test quite easily.

Một phần của tài liệu Manning the art of unit testing with examples in c sharp 2nd (Trang 141 - 144)

Tải bản đầy đủ (PDF)

(294 trang)