При взаимодействии с некоторыми разработчиками я заметил, что одной из причин, по которой они не проводят модульное тестирование существующего кода, является то, что они часто не знают, как преодолеть определенные проблемы. Наиболее распространенная из них связана с жестко привязанными зависимостями — синглетонами и статическими вызовами .
Давайте посмотрим на этот кусок кода:
public List<Trip> getTripsByUser(User user) throws UserNotLoggedInException {
List<Trip> tripList = new ArrayList<Trip>();
User loggedUser = UserSession.getInstance().getLoggedUser();
boolean isFriend = false;
if (loggedUser != null) {
for (User friend : user.getFriends()) {
if (friend.equals(loggedUser)) {
isFriend = true;
break;
}
}
if (isFriend) {
tripList = TripDAO.findTripsByUser(user);
}
return tripList;
} else {
throw new UserNotLoggedInException();
}
}
Ужасно, не так ли? Приведенный выше код имеет множество проблем, но прежде чем мы его изменим, мы должны покрыть его тестами
При модульном тестировании по методу выше есть две проблемы. Они есть:
User loggedUser = UserSession.getInstance().getLoggedUser(); // Line 3
tripList = TripDAO.findTripsByUser(user); // Line 13
Как мы знаем, модульные тесты должны тестировать только один класс, а не его зависимости. Это означает, что нам нужно найти способ издеваться над Синглтоном и статическим вызовом. В общем, мы делаем это, вводя зависимости, но у нас есть правило , помните?
Мы не можем изменить существующий код, если он не покрыт тестами. Единственное исключение — если нам нужно изменить код, чтобы добавить модульные тесты, но в этом случае допускаются только автоматические рефакторинги (через IDE).
Кроме того, многие из фальшивых фреймворков в любом случае не могут имитировать статические методы, поэтому внедрение TripDAO не решит проблему.
Преодоление проблемы жестких зависимостей
ПРИМЕЧАНИЕ. В реальной жизни я бы сначала писал тесты и вносил изменения именно тогда, когда мне было нужно, ноДля того, чтобы пост был коротким и целенаправленным, я не буду идти здесь шаг за шагом .
Прежде всего, давайте выделим зависимость Singleton от его собственного метода. Давайте сделаем это защищенным. Но подождите, это нужно сделать с помощью автоматического рефакторинга «метод извлечения». Выберите только следующий фрагмент кода на TripService.java:
UserSession.getInstance().getLoggedUser()
Перейдите в меню рефакторинга вашей IDE, выберите метод извлечения и присвойте ему имя. После этого шага код будет выглядеть так:
public class TripService {
public List<Trip> getTripsByUser(User user) throws UserNotLoggedInException {
...
User loggedUser = loggedUser();
...
}
protected User loggedUser() {
return UserSession.getInstance().getLoggedUser();
}
}
Делая то же самое для TripDAO.findTripsByUser (пользователь), мы получим:
public List<Trip> getTripsByUser(User user) throws UserNotLoggedInException {
...
User loggedUser = loggedUser();
...
if (isFriend) {
tripList = findTripsByUser(user);
}
...
}
protected List<Trip> findTripsByUser(User user) {
return TripDAO.findTripsByUser(user);
}
protected User loggedUser() {
return UserSession.getInstance().getLoggedUser();
}
В нашем тестовом классе мы можем теперь расширить класс TripService и переопределить созданные нами защищенные методы, заставляя их возвращать все, что нам нужно для наших модульных тестов:
private TripService createTripService() {
return new TripService() {
@Override protected User loggedUser() {
return loggedUser;
}
@Override protected List<Trip> findTripsByUser(User user) {
return user.trips();
}
};
}
И это все. Наш TripService теперь тестируемый.
Сначала мы пишем все тесты, которые нам нужны, чтобы убедиться, что класс / метод полностью протестирован и все ветви кода выполнены. Для этого я использую плагин Eclipse eclEmma , и я настоятельно рекомендую его. Если вы не используете Java и / или Eclipse, попробуйте использовать инструмент покрытия кода, специфичный для вашего языка / IDE, при написании тестов для существующего кода. Это очень помогает.
Итак, вот мой последний тестовый класс:
public class TripServiceTest {
private static final User UNUSED_USER = null;
private static final User NON_LOGGED_USER = null;
private User loggedUser = new User();
private User targetUser = new User();
private TripService tripService;
@Before
public void initialise() {
tripService = createTripService();
}
@Test(expected=UserNotLoggedInException.class) public void
shouldThrowExceptionWhenUserIsNotLoggedIn() throws Exception {
loggedUser = NON_LOGGED_USER;
tripService.getTripsByUser(UNUSED_USER);
}
@Test public void
shouldNotReturnTripsWhenLoggedUserIsNotAFriend() throws Exception {
List<Trip> trips = tripService.getTripsByUser(targetUser);
assertThat(trips.size(), is(equalTo(0)));
}
@Test public void
shouldReturnTripsWhenLoggedUserIsAFriend() throws Exception {
User john = anUser().friendsWith(loggedUser)
.withTrips(new Trip(), new Trip())
.build();
List<Trip> trips = tripService.getTripsByUser(john);
assertThat(trips, is(equalTo(john.trips())));
}
private TripService createTripService() {
return new TripService() {
@Override protected User loggedUser() {
return loggedUser;
}
@Override protected List<Trip> findTripsByUser(User user) {
return user.trips();
}
};
}
}
Мы все?
Конечно нет. Нам все еще нужно провести рефакторинг класса TripService. Проверьте вторую часть этого поста.
Если вы хотите попробовать, вот полный код: https://github.com/sandromancuso/testing_legacy_code
От http://craftedsw.blogspot.com/2011/07/testing-legacy-hard-wired-dependencies.html