При взаимодействии с некоторыми разработчиками я заметил, что одной из причин, по которой они не проводят модульное тестирование существующего кода, является то, что они часто не знают, как преодолеть определенные проблемы. Наиболее распространенная из них связана с жестко привязанными зависимостями — синглетонами и статическими вызовами .
Давайте посмотрим на этот кусок кода:
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