Статьи

Взаимодействие с трудным для тестирования сторонним кодом

Шахар задает отличный вопрос о том, как обращаться с фреймворками, которые мы используем в наших проектах, но которые не были написаны с учетом тестируемости.

Привет Миско. Прежде всего я хотел бы поблагодарить вас за « Руководство по написанию тестируемого кода », которое действительно помогло мне подумать о лучших способах организации своего кода и архитектуры. Пытаясь применить руководство к коду, над которым я работаю, я столкнулся с некоторыми трудностями. Наш код основан на внешних фреймворках и библиотеках. Зависимость от внешних платформ затрудняет написание тестов, поскольку настройка тестов намного сложнее. Мы используем не просто один класс, а целую кучу классов, базовых классов, определений и файлов конфигурации. Можете ли вы дать несколько советов по использованию внешних библиотек или сред, которые позволят легко тестировать код?

— Спасибо, Шахар

Вы можете попасть в две разные ситуации:

  1. Ваш код вызывает стороннюю библиотеку (например, вы используете аутентификацию LDAP или драйвер JDBC)
  2. Либо сторонняя библиотека вызывает вас и заставляет реализовать интерфейс или расширить базовый класс (например, при использовании сервлетов).

Если эти API не написаны с учетом тестируемости, они будут препятствовать вашей способности писать тесты.

Вызов сторонних библиотек

Я всегда стараюсь отделить себя от сторонней библиотеки с помощью Фасада и Адаптера. Фасад — это интерфейс, который имеет упрощенный вид стороннего API. Позвольте привести пример. Посмотрите на javax.naming.ldap . Это набор из нескольких интерфейсов и классов со сложным способом их вызова. Если ваш код зависит от этого интерфейса, вы утонете в адском насмешке. Теперь я не знаю, почему API такой сложный, но я знаю, что моему приложению нужна только часть этих вызовов. Я также знаю, что многие из этих вызовов зависят от конфигурации, и за пределами кода начальной загрузки эти API-интерфейсы загромождают то, что я должен макетировать.

Я начинаю с другого конца. Я задаю себе этот вопрос. «Как будет выглядеть идеальный API для моего приложения?» Ключевым моментом здесь является «мое приложение». Приложение, которому требуется только аутентификация, будет иметь совершенно иной «идеальный API», чем приложение, которое должно управлять LDAP. Поскольку мы ориентируемся на наше приложение, полученный API значительно упрощается. Вполне возможно, что для большинства приложений идеальным интерфейсом может быть нечто подобное.

interface Authenticator {
  boolean authenticate(String username,
                       String password);
}

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

Когда у нас есть идеальный интерфейс, все, что нам нужно сделать, это реализовать адаптер, который соединяет наш идеальный интерфейс с реальным. Этот адаптер может быть болью для тестирования, но, по крайней мере, боль находится в одном месте.

Преимущество этого заключается в том, что:

  • Мы можем легко реализовать InMemoryAuthenticator для запуска нашего приложения в среде QA.
  • Если сторонние API меняются, эти изменения влияют только на код нашего адаптера.
  • Если теперь нам нужно пройти аутентификацию в реестре Kerberos или Windows, реализация будет проста.
  • Мы с меньшей вероятностью представим ошибку использования, поскольку вызов идеального API проще, чем вызов исходного API.

Подключение к существующей платформе

Давайте возьмем сервлеты в качестве примера сложных для тестирования фреймворков. Почему сервлеты сложно тестировать?

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

На высоком уровне я использую ту же стратегию отделения себя от API сервлетов. Я реализую свои действия в отдельном классе

class LoginPage {
  Authenticator authenticator;
  boolean success;
  String errorMessage;
  LoginPage(Authenticator authenticator) {
    this.authenticator = authenticator;
  }

  String execute(Map<String, String> parameters,
                 String cookie) {
    // do some work
    success = ...;
    errorMessage = ...;
  }

  String render(Writer writer) {
    if (success)
      return "redirect URL";
    else
      writer.write(...);
  }
}

Код выше легко проверить, потому что:

  • Он не наследуется ни от какого базового класса.
  • Внедрение зависимостей позволяет нам вводить фиктивный аутентификатор (в отличие от конструктора без аргументов в сервлетах).
  • Фаза работы отделена от фазы рендеринга. На Writer действительно сложно утверждать что-либо полезное, но мы можем утверждать о состоянии LoginPage , например success и errorMessage .
  • Входные параметры в LoginPage очень легко создать. ( Карта <String, String> , String для файла cookie или StringWriter для автора).

Мы достигли того, что вся логика нашего приложения находится в LoginPage, а весь непроверяемый беспорядок — в LoginServlet, который действует как адаптер. Мы можем чем глубоко проверить LoginPage . LoginSevlet не так просто, и в большинстве случаев я просто не беспокоить тестирование , так как там может быть только проводки ошибки в этом коде. В LoginServlet не должно быть логики приложения, поскольку мы переместили всю логику приложения в LoginPage .

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

class LoginServlet extends HttpServlet {
  Provider<LoginPageProvider> loginPageProvider;

  // no arg constructor required by
  // Servlet Framework
  LoginServlet() {
    this(Global.injector
           .getProvider(LoginServlet.class));
  }

  // Dependency injected constructor used for testing
  LoginServlet(Provider<LoginPage> loginPageProvider) {
    this.loginPageProvider = loginPageProvider;
  }

  service(HttpServletRequest req,
          HttpServletResponse resp) {
    LoginPage page = loginPageProvider.get();
    page.execute(req.getParameterMap(),
         req.getCookies());
    String redirect = page.render(resp.getWriter())
    if (redirect != null)
      resp.sendRedirect(redirect);
  }
}

Обратите внимание на использование двух конструкторов. Одна зависимость полностью введена, а другая без аргумента. Если я напишу тест, я буду использовать конструктор зависимостей, который позволит мне смоделировать все мои зависимости.

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

Кстати, есть много фреймворков, которые работают поверх сервлетов и предоставляют вам очень тестируемые API. Все они достигают этого, отделяя вас от реализации сервлета и от HttpServletRequest и HttpServletResponse . Например, вафли и WebWork

С http://misko.hevery.com/