Насмешка над синглтоном для модульного тестирования

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

Я новичок в JMockit, и я попробовал это, но безуспешно: вот исходный класс, над которым нужно издеваться:

public final class PageManager {
  private static final PageManager INSTANCE = new PageManager();

  private final Set<Page> pages = new HashSet<>();
  private Page currentPage;
  private boolean initialized = false;

  private PageManager() {
    // Do some UI stuff
  }

  public static PageManager getInstance() {
    return INSTANCE;
  }

  public void addPage(final Page page) {
    pages.add(page);
  }

  public void initialize() {
    // Do some UI stuff
    initialized = true;
  }

  public Page getPage() { return currentPage; }
  public void setPage(final Page page) { ... }

  public void goBack() { ... };
  public void goForward() { ... };
  public boolean canGoBack() { ... };
  public boolean canGoForward() { ... };

  private void activatePage(final Page page) {
    // Do some UI stuff
    this.currentPage = page;
  }

  private void deactivatePage(final Page page) {
    // Do some UI stuff
  }
}

Это издевательская версия:

public final class MockedPageManager extends MockUp<PageManager> {
  PageManager instance;

  @Mock
  void $init(final Invocation invocation) {
    instance = invocation.getInvokedInstance();
  }

  @Mock
  void initialize() {
    // Don't do UI stuff
    Deencapsulation.setField(instance, "initialized", true);
  }

  @Mock
  void activatePage(Page page) {
    Deencapsulation.setField(instance, "currentPage", page);
    page.activate();
  }

  @Mock
  void deactivatePage(Page page) {
  }
}

И небольшой тест:

@Test
public void testGoBack() {
  new MockedPageManager();

  final Page p1 = new Page() { @Override public String getTitle() { return "p1"; } };
  final Page p2 = new Page() { @Override public String getTitle() { return "p2"; } };

  final PageManager pm = PageManager.getInstance();
  pm.addPage(p1);
  pm.addPage(p2);
  pm.initialize();

  pm.setPage(p1)
  assertEquals(p1, pm.getCurrentPage());
  pm.setPage(p2);
  assertEquals(p2, pm.getCurrentPage())
  assertTrue(pm.canGoBack());
  pm.goBack();
  assertEquals(p1, pm.getCurrentPage());
}

В этом тесте JMockit правильно вызывает метод $init. Проблема в том, что при вызове pm.addPage(p1) в тесте выдается NullPointerExceptions. Трассировка стека говорит, что NPE происходит в исходном классе PageManager, потому что поле Set pages равно null.

Мой вопрос: правильно ли высмеян этот одноэлементный класс? Метод $init переопределяет только конструктор или также инициализатор экземпляра Java, т.е. Set pages = new HashSet‹>();


person Vertex    schedule 18.09.2013    source источник
comment
Оставьте одиночек в покое! Что они когда-либо делали с тобой? Извините, не удержался. Продолжать..   -  person Alec.    schedule 18.09.2013
comment
Я клянусь, что буду избегать синглетонов и заменю их на DI или что-то подобное :)   -  person Vertex    schedule 18.09.2013


Ответы (1)


Как указано здесь, блоки или операторы инициализации экземпляра копируется в каждый конструктор (компилятором). Я подозреваю, что JMockit использует манипулирование кодом отражения/байта, чтобы издеваться над конструктором класса, эффективно обходя весь код инициализации. Поэтому инициализаторы не выполняются, и установленная переменная остается нулевой. Если вам действительно нужно выполнить эту работу, попробуйте правильно инициализировать ее в макете. А еще лучше реорганизуйте свой класс, чтобы разрешить его использование в тестах (например, добавьте дополнительный закрытый конструктор пакета для тестирования с внедренными зависимостями или переместите функциональность истории страниц в свой собственный класс).

person Pyranja    schedule 18.09.2013
comment
Большое спасибо! Я добавил Deencapsulation.setField(instance, "pages", new HashSet<Page>()); в метод $init, и он работает хорошо. Вау, я в восторге от того, что можно протестировать такие классы, ничего не меняя. Очень хорошо :) - person Vertex; 18.09.2013