Статьи

Тестирование наследия: аппаратные зависимости (часть 1)

При взаимодействии с некоторыми разработчиками я заметил, что одной из причин, по которой они не проводят модульное тестирование существующего кода, является то, что они часто не знают, как преодолеть определенные проблемы. Наиболее распространенная из них связана с жестко привязанными зависимостями — синглетонами и статическими вызовами .

Давайте посмотрим на этот кусок кода:

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