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

Chapter 7. Points of Controversy

7.6. New Operator

A seam is a place where you can alter behavior in your program without editing in that place.

— Michael Feathers Working Effectively With Legacy Code (2004)

When introducing test doubles (in Chapter 5, Mocks, Stubs, Test Spies), we assumed that the collaborators of the SUT could be replaced easily. This is the case if they are "injected" into the SUT - as constructor parameters, method parameters or using setters. However, this is not always the case. This section explains what can be done if there is no straightforward way of replacing the SUT’s collaborators.

Listing 7.13 presents some very simple code. The myMethod() method of the MySut class creates another object (of the MyCollaborator class) using the new operator. Then it uses this newly created object (calls its methods, uses values it returns, etc.).

Listing 7.13. Typical use of the Java new operator within the method body

public class MySut {

public void myMethod() {

MyCollaborator collaborator = new MyCollaborator();

// some behaviour worth testing here which uses collaborator

}

}

The code in Listing 7.13 is perfectly valid Java code and, frankly, not complicated either. However, testing interactions between the SUT and its collaborator requires some additional effort. A test skeleton for such a method would look as shown below:

Listing 7.14. Testing new - a test skeleton

public class MySutTest {

@Test

public void testMyMethod() { MySut sut = new MySut();

MyCollaborator collaborator = mock(MyCollaborator.class);

//make sut use collaborator

//set expectations regarding collaborator's behaviour

//execute sut's method(s)

//verify results and/or collaborator's behaviour

}

}

Creation of the SUT.

Creation of a test double for the SUT’s collaborator. "Make sut use collaborator" - but how?!

When writing a test for the MySut class, we very quickly encounter a problem. There is no direct way to force sut to use a test double of the MyCollaborator class. This means that we cannot easily control the SUT’s environment. One option we have is to forget about testing in isolation, and test both classes (MySut and MyCollaborator) together. While this might work out pretty well in the short run (see the discussion in Section 5.5), it makes it hard to test all of the scenarios we should test. We don’t need to relive the whole debate about this: isolation in tests is usually worth fighting for, so let us fight for it!

160

Chapter 7. Points of Controversy

Even though myMethod() uses the new operator to create its collaborator, the discussion in this section also covers other similar situations. In particular, if myMethod() had used

a factory pattern (e.g. MyCollaborator collaborator = MyFactory.getCollaborator(…)),

instead of the new operator, or had used a lookup service (e.g. MyCollaborator collaborator = LookupService.findCollaborator(…)), that would have caused exactly the same problem when testing. The solutions discussed in the following sections would also apply to these similar scenarios.

Now that we understand the problem, let us discuss possible solutions to it. Each of them has its own pros and cons, which will be discussed in the sections below. But before we take a closer look at them, let us get clear about exactly what kind of a dilemma we are faced with here.

As was stated in Section 1.3, tools for testing can be divided into two types. Some of them only deal with verification, while others are more concerned with design. Mockito belongs to the second group: it works perfectly with well-written, loosely coupled code, but "refuses" to test code that is tightly-coupled. The problem we are facing right now – the use of either the new operator or static methods - is precisely a representation of what happens when the code is not loosely coupled. The ties between objects (in this case between sut and collaborator) are so tight that it is hard to loosen them for testing purposes.

Having read this, one might well expect the solutions called for to come from using both types of tool. And this is exactly the case. Either we can use a tool that allows us to test tightly coupled code, or we will find it necessary to introduce some changes to the original code (hopefully making it better), and then use a tool from the second group.

7.6.1. PowerMock to the Rescue

We shall start with a tool which can simply ignore the nuisance that is the new operator, and create a test double, as if that were no issue at all. Such "magic" is possible with PowerMock.

Listing 7.15 presents a test class which uses PowerMock. It does not differ substantially from what we have encountered so far, but uses some annotations and classes which we have not yet come across.

PowerMock acts as a kind of wrapper around different mocking frameworks. It enhances their functionality with some new features. I will not go deeply into the details of the PowerMock syntax, nor will I discuss all of its capabilities. Please refer to PowerMock’s documentation for the full list of features, and detailed instructions on how to use it with Mockito and JUnit.

161

Chapter 7. Points of Controversy

Listing 7.15. Using PowerMock to test the new operator

import org.powermock.api.mockito.PowerMockito;

import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.PowerMockRunner;

@PrepareForTest(MySut.class) @RunWith(PowerMockRunner.class) public class MySutTest {

@Test

public void testMyMethod() throws Exception { MySut sut = new MySut();

MyCollaborator collaborator = mock(MyCollaborator.class);

PowerMockito.whenNew(MyCollaborator.class).

withNoArguments().thenReturn(collaborator);

//normal test using Mockito's syntax

//e.g. Mockito.when(collaborator.someMethod()).thenReturn(...)

}

}

Required imports.

In this case, the @PrepareForTest annotation informs PowerMock that the MySut class will create a new instance of some other class. In general, this is how PowerMock learns, about which classes it should perform some bytecode manipulation on.

A special runner is used so PowerMock can be used within JUnit tests.

The test double is created as usual - with the static mock() method of Mockito.

This is where the magic happens: whenever a new object of the MyCollaborator class gets created, our test double object (collaborator) will be used instead. Two of PowerMock’s methods - whenNew() and withNoArguments() - are used to control the execution of a no-arguments constructor

of the MyCollaborator class.

There is nothing special in the test method itself - we can use normal Mockito syntax to verify the behaviour, to stub or to spy.

And that is it. Except for some annotations required in order to use PowerMock and JUnit together, there is not much new here: only that in some places the PowerMockito class is used instead of the Mockito class. Apart from this, the code looks very similar to what we have come across so far.

Let us conclude by summing up what this example has showed us:

there are tools (i.e. PowerMock) capable of dealing with the new operator, static method calls, and other language constructs, which are commonly recognized as being "untestable",

it is possible to test classes like the MySut class without changing (refactoring or redesigning) them at all,

PowerMock works nicely with JUnit and Mockito,

tests written with PowerMock do not differ much from what we have become used to.

After seeing what PowerMock can do for us, we should answer one question, which is surely shouting in our heads: "Why bother with anything else if I can do it so easily using PowerMock?!"

There is a serious reason for avoiding using such tools. It is all about the quality of your code - in particular, about its maintainability. By using PowerMock as shown in Listing 7.15, you reproduce in

162

Chapter 7. Points of Controversy

your test code the tight-coupling which exists in the production code. Now, every time you change your production code, your tests will fail. By including implementation details in your tests (as PowerMock requires you to do) you have created an additional force which will make it harder to introduce changes to your production code. This is not good.

Additionally, PowerMock lets you get away with suboptimal design. For example, a class can have way too much responsibility and still be capable of being tested with PowerMock. Testing it with other tools would be very cumbersome, but with PowerMock you will not feel the pain. In short, using PowerMock deprives you of valuable feedback about the quality of your code, which other tools will give you.

However, there are situations where having PowerMock in your toolbox is a real blessing. That is why I have decided to present this tool, even though I myself do not use it on a daily basis.

7.6.2. Redesign and Inject

Now we will be going in a direction quite opposite to that which we went in with PowerMock: we shall be working on the production code, to make it more testable. After this goal has been accomplished, we will be able to use a common Mockito approach, without using any reflection or class loading tricks.

Basically, there are two ways in which a collaborator object can be replaced easily with a test double:

create a new field in a MySut class of the MyCollaborator type, and

either pass an object of the MyCollaborator class as the constructor’s argument, or via a setter method,

or redesign myMethod() so it takes an object of the MyCollaborator class as one of it arguments.

No matter which option we choose, writing test code will then be a piece of cake. Listing 7.16 shows a refactored MySut class (with a constructor-injected collaborator), and Listing 7.17 shows a test written for this class.

Listing 7.16. MySut class with constructor-injected collaborator

public class MySut {

private final MyCollaborator collab;

public MySut(MyCollaborator collab) { this.collab = collab;

}

public void myMethod() {

// some behaviour worth testing here which uses collaborator

}

}

An object of the MyCollaborator class is provided by MySut's clients when they create objects of the MySut class.

No new operator is used within myMethod(). The collaborator object has been created outside of the MySut class and passed as a constructor parameter.

The myMethod() method does not deal with the creation of collaborators anymore. It uses a collab object of the MyCollaborator class, which is already available when myMethod() is executed.

163

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