Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Tomek Kaczanowski - Practical Unit Testing with JUnit and Mockito - 2013.pdf
Скачиваний:
224
Добавлен:
07.03.2016
Размер:
6.59 Mб
Скачать

Chapter 7. Points of Controversy

Listing 7.17. Testing of the redesigned MySut class

public class MySutTest {

@Test

public void testMyMethod() {

MyCollaborator collaborator = mock(MyCollaborator.class); MySut sut = new MySut(collaborator);

// normal Mockito stubbing/test spying test

}

}

The updated constructor of the MySut class allows for straightforward injection of a collaborator test double.

The test in Listing 7.17 holds no surprises for us. Replacing the collaborator of sut is simply a matter of passing the appropriate test double to its constructor.

As this example demonstrates, a complicated case can be turned into something far simpler (from a testing point of view) by the redesign of the SUT. If it is possible to replace the cumbersome creation of objects (involving either the new operator or static factory methods) with a dependency injection, then testing becomes a trivial task.

Even so, there are some downsides – or, at least, some issues we should be aware of:

production code gets altered to meet the requirements of the tests themselves (isn’t this a case of the tail wagging the dog?),

such a redesign breaks existing clients (it truly is a redesign, not a refactoring),

you need to write some additional code.

On the "plus" side, there are two important things:

the design of production code is improved (myMethod() can take care of its business task, and need not occupy itself with objects creation),

tests are very simple to write (which also means they are easy to understand and maintain).

After we have discussed all of the possible options, some conclusions will be drawn, but even now, I would like to encourage you to use this technique, because it addresses the real cause of the pain, not merely its symptoms. By redesigning your production code you make it better, and you get "testability" as a side effect.

7.6.3. Refactor and Subclass

In some cases the redesign discussed in the previous section is not feasible. For example, it might be the case that we cannot afford to break a client that uses the old API. In such a case, we need to find another way to make the class more testable. This can be done by a certain refactoring.

The pattern presented in this section is known by many names. [meszaros2007] calls this pattern a Test-Specific Subclass, while [feathers2004] uses the name subclass and override.

164

Chapter 7. Points of Controversy

Listing 7.18 shows a slightly changed version of the original MySut class.

Listing 7.18. Refactored version of the MySut class

public class MyRefactoredSut {

public void myMethod() {

MyCollaborator collaborator = createCollaborator();

// some behaviour worth testing here which uses collaborator

}

// method extracted to facilitate testing protected MyCollaborator createCollaborator() {

return new MyCollaborator();

}

}

Instead of invoking the new operator directly there is a call to a new method.

I strongly recommend adding a short comment to the extracted method, so it is clear that it has been created for the purpose of conducting unit tests.

The extracted method has a default access modifier, …and not very impressive content.

As for the access modifier of the extracted createCollaborator() method, we can choose between making it protected or using default access protection (so-called "package-private"). This has some impact on our test classes. If we keep them in the same package as the production code, then the default access will be good enough. If they reside in different packages, then we will need to make the access less restrictive (protected). Some information on the organization of test packages is given in Section 9.1.

The change introduced to the original class is minimal. All we did is a simple "extract method" refactoring. At first it might seem useless, but in fact it opens up a new testing possibility. Listing 7.19 shows a test which takes advantage of this minor refactoring.

Listing 7.19. Test of the refactored version of the MySut class

public class MySutRefactoredTest {

private MyCollaborator collaborator;

class MyRefactoredSutSubclassed extends MyRefactoredSut { @Override

protected MyCollaborator createCollaborator() { return collaborator;

}

}

@Test

public void testMyMethod() {

MyRefactoredSut sut = new MyRefactoredSutSubclassed(); collaborator = mock(MyCollaborator.class);

when(collaborator.someMethod()).thenReturn(true); assertTrue(sut.myMethod());

}

}

A new class is introduced - a subclass of the original class (the one we intend to test). An object of this newly introduced class - MyRefactoredSutSubclassed - will be tested.

165

Chapter 7. Points of Controversy

The new class overrides the createCollaborator() method. The new implementation returns a test double of the MyCollaborator class.

There is no need to inject collaborator to sut - this is done by a myMethod() of SUT’s parent class (see Listing 7.18).

This simple technique, as presented in this section, can be really handy. Let us conclude by now listing some of its pros and cons:

It does not break the clients (the SUT’s API has not been changed) and it lets you write tests at the same time.

Changes are limited to the SUT class.

The design of production code is somewhat worse than it was before the refactoring. The newly introduced method is awkward, and poses a potential encapsulation issue. (I must remark that I have never seen any harm being done because of this slight reduction in the SUT’s integrity.)

The SUT’s myMethod() still creates collaborators (instead of focusing on its business tasks), which means its design has not been improved.

Some people feel bad about testing a subclass instead of a real SUT. Personally, I would be very happy to test the real thing, but just having a test of a strictly controlled subclass has proved more than adequate for my purposes.

7.6.4. Partial Mocking

The technique demonstrated in this section is very similar to the previously presented subclass and override technique. The difference lies in the implementation, which requires less coding and relies on some Mockito features.

The first step is almost identical to the one previously discussed. You must extract a method which will deal solely with the creation of a new object. Listing 7.20 shows this.

Listing 7.20. Refactored version of the MySut class

public class MyPartialSut {

public boolean myMethod() {

MyCollaborator collaborator = createCollaborator();

// some behaviour worth testing here which uses collaborator

}

// method extracted to facilitate testing MyCollaborator createCollaborator() {

return new MyCollaborator();

}

}

Extracted method.

So far, nothing new here. The difference is only visible when it comes to the tests class. But before we inspect any new code, we need to learn something new about Mockito. Mockito allows us to "spy" on real objects. That means we can decide which methods of a real object are invoked, and which are

166

Chapter 7. Points of Controversy

being intercepted (and stubbed). The Javadocs of the spy() method explains this in the following way:

"Creates a spy of the real object. The spy calls real methods unless they are stubbed."

This feature is deployed in Listing 7.21, which illustrates the use of a partial mocking technique. Additionally, it presents a new static Mockito method - doReturn(). We use

doReturn(collaborator).when(sut).createCollaborator();

instead of

when(collaborator.someMethod()).thenReturn(true);

to avoid execution of the real method (someMethod()), because:

the real method might throw some exceptions,

if we are to verify it, then the number of invocations would grow.

Listing 7.21. Partial mocking test of the refactored MySut class

public class MySutPartialTest {

@Test

public void testMyMethod() {

MyPartialSut sut = spy(new MyPartialSut()); MyCollaborator collaborator = mock(MyCollaborator.class);

doReturn(collaborator).when(sut).createCollaborator(); // normal Mockito stubbing/test spying test

}

}

Surprise! The SUT has not been created using the new operator. Instead, another static method of Mockito has been used.

Another unusual situation. We request that our SUT returns some canned values for a given method invocation.

As was mentioned before, when created with the spy() method sut behaves like a normal object of its class, until some of its methods are stubbed. In the case of our example, all the SUT’s methods, except for createCollaborator(), would be executed as if sut had been created with the new keyword. However, this one method - createCollaborator() - is different. When createCollaborator() is executed, it is intercepted by Mockito, which returns some canned values without touching the real sut object at all.

Well, this is confusing! Our SUT is performing two roles here: first of all, it is being tested (which is not shown in the above listing, for reasons of brevity), and secondly, it is being used as a test stub, to provide some canned values.

Partial mocking using the spy() method has its quirks. Make sure you read the Mockito documentation before using it!

As for pros and cons, this technique is very similar to the one discussed previously, with the following differences:

• There is no need to create a subclass. This is done "under the hood" by Mockito.

167

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]