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

Chapter 7. Points of Controversy

Redesign is probably the right way to go, but it requires more work than other techniques, and breaks clients (because of API change). Its main benefit is improved design of production code.

Both Refactor & Subclass and Partial Mocking offer similar benefits: the ability to test classes without API change. However, they require some effort, cannot be used with final classes, and do not make production code better (they rather make it slightly worse).

To conclude, I would strongly recommend that you always aim at making the design better, thus leaving PowerMock as a tool of "last resort" – one that may save your life when battling with especially vicious legacy code.

7.7. Capturing Arguments to Collaborators

Sometimes there is a need to verify whether arguments passed to collaborators are exactly as they should be. This might be tricky, especially if they are created within the SUT’s method. In this section we will discuss possible solutions which allow us to test such code.

First, let us meet the classes which will be used to demonstrate this issue. There are three of them:

PIM - which represents a Personal Information Manager10. This is the SUT.

Calendar - a collaborator of PIM.

Meeting - argument to Calendar's method call.

All three types are shown in the listings below:

Listing 7.22. Calendar interface

public interface Calendar {

public void addEvent(Event event);

}

Listing 7.23. Meeting class

public class Meeting implements Event {

private final Date startDate; private final Date endDate;

public Meeting(Date startDate, Date endDate) { this.startDate = new Date(startDate.getTime()); this.endDate = new Date(endDate.getTime());

}

public Date getStartDate() { return startDate;

}

public Date getEndDate() { return endDate;

}

}

10http://en.wikipedia.org/wiki/Personal_information_manager

169

Chapter 7. Points of Controversy

Listing 7.24. PIM - the sut

public class PIM {

private final static int MILLIS_IN_MINUTE = 60 * 1000;

private Calendar calendar;

public PIM(Calendar calendar) { this.calendar = calendar;

}

public void addMeeting(Date startDate, int durationInMinutes) { Date endDate = new Date(startDate.getTime()

+ MILLIS_IN_MINUTE * durationInMinutes); Meeting meeting = new Meeting(startDate, endDate); calendar.addEvent(meeting);

}

}

The collaborator is passed as a constructor argument - there will be no problem with injecting its test double into the SUT within the test code.

A new object of the Meeting type is created, and is then used as a parameter to the collaborator’s

addEvent() method.

The problem we face when testing the PIM class is the following: how to make sure that the addMeeting() method constructs a proper Meeting object? We need to somehow intercept the parameter passed to calendar.addEvent(). There are two ways we can do it.

The problem with addMeeting() method testing comes from its poor design. It is responsible for too many things - it deals with the creation of the Meeting object, and interacts with the calendar collaborator. If we were to split its functionality and, for example, introduce another collaborator responsible for the creation of proper Meeting objects, than there would be no issue with testing arguments of the addEvent() method!

7.7.1. Capturing Arguments - Creating Identical

Objects

How about creating an object of Meeting class identical to the one which we expected would be created by the addMeeting() method of SUT? Then we can verify whether the addEvent() method of the calendar test double has been called with an identical Meeting object. Sounds good, so let us try it!

170

Chapter 7. Points of Controversy

Listing 7.25. Creating objects identical to expected arguments

public class PIMTest {

private static final int ONE_HOUR = 60;

private static final Date START_DATE = new Date(); private static final int MILLIS_IN_MINUTE = 1000 * 60;

private static final Date END_DATE = new Date(START_DATE.getTime() + ONE_HOUR * MILLIS_IN_MINUTE);

@Test

public void shouldAddNewEventToCalendar() { Calendar calendar = mock(Calendar.class); PIM pim = new PIM(calendar);

Meeting expectedMeeting = new Meeting(START_DATE, END_DATE);

pim.addMeeting(START_DATE, ONE_HOUR);

verify(calendar).addEvent(expectedMeeting);

}

}

An object of the Meeting class is created, identical to the one which we expect to be created by SUT’s addMeeting() method.

Execution of the method of the SUT being tested.

Verification of whether the calendar.addEvent() method has been called with exactly the same

Meeting object.

Looks good? Yes, but unfortunately it does not work. The test fails with the following failure message (stacktrace lines have been omitted for greater readability):

Listing 7.26. The test fails - objects are not identical

Argument(s) are different! Wanted: calendar.addEvent(

com.practicalunittesting.Meeting@1242b11

);

Actual invocation has different arguments: calendar.addEvent(

com.practicalunittesting.Meeting@1878144

);

Hmm, strange! I could have sworn that the addMeeting() method constructs a proper Meeting object… And in fact it does! The problem lies elsewhere. The Meeting class does not override the equals() method, so the objects’ equality is verified by reference, and thus fails. We can fix it by adding appropriate methods11 to the Meeting class. Below you can see an implementation of the equals() method generated by IntelliJ IDEA:

11Remember, if you override equals() you should also override hashCode()!

171

Chapter 7. Points of Controversy

Listing 7.27. Implementation of the equals() method

@Override

public boolean equals(Object o) { if (this == o) return true;

if (o == null || getClass() != o.getClass()) return false;

Meeting meeting = (Meeting) o;

if (endDate != null ? !endDate.equals(meeting.endDate) : meeting.endDate != null) return false;

if (startDate != null ? !startDate.equals(meeting.startDate) : meeting.startDate != null) return false;

return true;

}

@Override

public int hashCode() { ... }

The hashCode() method implementation is not important right now, but remember: it should also be overridden!

Now, if we rerun the test, it passes. Good, we have the first solution to the problem of verifying whether the arguments passed to the collaborator are as expected.

However, this is not an ideal solution. On the plus side, we may say that:

it works!

it is quite straightforward and easy to understand.

Unfortunately, there are more things to say on the minus side:

A domain object in question might not have the equals() method implemented.

In worse cases, a domain object’s equals() might already be implemented, and might behave differently than as required by our test.

The verification is "total" - it checks everything that our equals() method checks. This might lead to overspecified tests.

There is no direct assertion on the properties of the collaborator’s method argument.

Because of these downsides, we need to find some better solution.

7.7.2. Capturing Arguments - Using Mockito’s

Features

Listing 7.28 shows another approach to writing a test for arguments of the collaborator’s method. It uses the Mockito ArgumentCaptor class.

172

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