Singleton и модульное тестирование

Эффективная Java имеет следующее утверждение о единичных тестовых синглонах

Создание класса singleton может затруднить проверку его клиентов, поскольку его невозможно заменить макетной реализацией для singleton, если только он не реализует интерфейс, который служит его типом.

Может ли кто-нибудь объяснить, почему это так?

Ответ 1

Вы можете использовать рефлексию, чтобы сбросить свой объект singleton, чтобы тесты не влияли друг на друга.

@Before
public void resetSingleton() throws SecurityException, NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
   Field instance = MySingleton.class.getDeclaredField("instance");
   instance.setAccessible(true);
   instance.set(null, null);
}

Ref: unit-testing-singletons

Ответ 2

Mocks требует интерфейсов, потому что то, что вы делаете, заменяет реальное основное поведение самозванцем, который имитирует то, что вам нужно для теста. Поскольку клиент имеет дело только с типом ссылки на интерфейс, ему не нужно знать, что такое реализация.

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

Это верно для всех классов, Singleton или нет.

Ответ 3

Я думаю, что это действительно зависит от реализации шаблона доступа singleton.

Например

MySingleton.getInstance()

Может быть очень сложно проверить, пока

MySingletonFactory mySingletonFactory = ...
mySingletonFactory.getInstance() //this returns a MySingleton instance or even a subclass

Не предоставляет никакой информации о том, что он использует одноэлемент. Таким образом, вы можете свободно заменить свой завод.

ПРИМЕЧАНИЕ: singleton определяется как только один экземпляр этого класса в приложении, однако способ, которым он был получен или сохранен, не должен проходить через статические средства.

Ответ 4

Это так просто.

В блоке -testing вы хотите изолировать свой SUT (класс, который вы тестируете). Вы не хотите тестировать кучу классов, потому что это приведет к поражению цели подразделения -testing.

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

Дело в том, что вы не заботитесь о том, как классы вашего SUT зависят от работы. Вы заботитесь о том, как ваш SUT работает с этими классами. Вот почему вы заглушаете или высмеиваете классы, которые нужны вашему SUT. И вы можете использовать эти mocks, потому что вы можете передать их в качестве параметров конструктора для вашего SUT.

С синглтонами - плохо то, что метод getInstance() доступен на глобальном уровне. Это означает, что вы обычно вызываете его из класса, а не в зависимости от интерфейса, который вы можете позже высмеять. Поэтому невозможно заменить его, если вы хотите протестировать ваш SUT.

Решение заключается не в использовании public static MySingleton getInstance(), а в зависимости от интерфейса, с которым должен работать ваш класс. Сделайте это, и вы можете пройти в тестовом удваивании всякий раз, когда вам нужно.

Ответ 5

Проблема не в том, чтобы тестировать сами одиночки; в книге говорится, что если класс, который вы пытаетесь проверить, зависит от синглтона, тогда у вас, скорее всего, будут проблемы.

Если, то есть, вы (1) не делаете singleton реализовывать интерфейс и (2) вводите singleton в свой класс с использованием этого интерфейса.

Например, синглтоны обычно создаются непосредственно так:

public class MyClass
{
    private MySingleton __s = MySingleton.getInstance() ;

    ...
}

MyClass теперь может быть очень сложно автоматизированно тестировать. Например, как отмечает в своем ответе @Boris Pavlović, если поведение singleton основано на системном времени, ваши тесты теперь также зависят от системного времени, и вы не сможете тестировать случаи, которые, скажем, зависят от день недели.

Однако, если ваш синглтон "реализует интерфейс, который служит его типом", вы все равно можете использовать одноэлементную реализацию этого интерфейса, если вы передаете его:

public class SomeSingleton
    implements SomeInterface
{
    ...
}

public class MyClass
{
    private SomeInterface __s ;

    public MyClass( SomeInterface s )
    {
        __s = s ;
    }

    ...
}

...

MyClass m = new MyClass( SomeSingleton.getInstance() ) ;

С точки зрения тестирования MyClass вам теперь все равно, является ли SomeSingleton синглтонным или нет: вы также можете передать любую другую реализацию, которая вам нужна, включая реализацию singleton, но, скорее всего, вы будете использовать макет какого-то типа, который вы контролируете от ваших тестов.

Кстати, это НЕ способ сделать это:

public class MyClass
{
    private SomeInterface __s = SomeSingleton.getInstance() ;

    public MyClass()
    {
    }

    ...
}

Это все равно работает во время выполнения, но для тестирования вы теперь снова SomeSingleton от SomeSingleton.

Ответ 6

Объекты Singleton создаются без какого-либо контроля извне. В одной из других глав той же книги Блох предлагает использовать enum качестве реализации Singleton по умолчанию. Рассмотрим пример

public enum Day {
  MON(2), TUE(3), WED(4), THU(5), FRI(6), SAT(7), SUN(1);

  private final int index;

  private Day(int index) {

    this.index = index;
  }

  public boolean isToday() {

    return index == new GregorianCalendar().get(Calendar.DAY_OF_WEEK);
  }
}

Скажем, у нас есть код, который должен исполняться только по выходным:

public void leisure() {

  if (Day.SAT.isToday() || Day.SUN.isToday()) {

    haveSomeFun();
    return;
  }

  doSomeWork();
}

Тестирование метода досуга будет довольно сложным. Его выполнение будет зависеть от дня его выполнения. Если он выполняется в будний день, то doSomeWork() будет вызываться, а в выходные дни haveSomeFun().

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

Ответ 7

its impossible to substitute a mock implementation for a singleton

Это неправда. Вы можете подклассировать свой синглтон, а сеттер - вводить макет. Кроме того, вы можете использовать PowerMock для моделирования статических методов. Однако необходимость издеваться над одиночными моделями может быть симптомом плохого дизайна.

Реальная проблема - синглтоны, когда они злоупотребляют, превращаются в магниты зависимостей. Поскольку они доступны повсюду, может оказаться более удобным разместить нужные вам функции, а не делегировать соответствующий класс, особенно для программистов, новых для ООП.

Проблема проверки - теперь у вас есть группа синглтонов, к которым обращается ваш тестируемый объект. Несмотря на то, что объект, возможно, использует только небольшую часть методов в синглтонах, вам все равно нужно высмеять каждый Синглтон и выяснить, от каких методов зависит. Синглтоны со статическим состоянием (Моностатный шаблон) еще хуже, потому что вам нужно выяснить, какие взаимодействия между объектами затронуты состоянием Singleton.

Используемые тщательно, синглтоны и проверяемость могут возникать вместе. Например, в отсутствие рамки DI вы можете использовать Singletons как свои фабрики и ServiceLocators, которые вы можете настроить для создания поддельного уровня обслуживания для ваших сквозных тестов.

Ответ 8

Возможно, см. Пример

import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import java.lang.reflect.Field;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;

public class DriverSnapshotHandlerTest {

private static final String MOCKED_URL = "MockedURL";
private FormatterService formatter;

@SuppressWarnings("javadoc")
@Before
public void setUp() {
    formatter = mock(FormatterService.class);
    setMock(formatter);
    when(formatter.formatTachoIcon()).thenReturn(MOCKED_URL);
}

/**
 * Remove the mocked instance from the class. It is important, because other tests will be confused with the mocked instance.
 * @throws Exception if the instance could not be accessible
 */
@After
public void resetSingleton() throws Exception {
   Field instance = FormatterService.class.getDeclaredField("instance");
   instance.setAccessible(true);
   instance.set(null, null);
}

/**
 * Set a mock to the {@link FormatterService} instance
 * Throws {@link RuntimeException} in case if reflection failed, see a {@link Field#set(Object, Object)} method description.
 * @param mock the mock to be inserted to a class
 */
private void setMock(FormatterService mock) {
    Field instance;
    try {
        instance = FormatterService.class.getDeclaredField("instance");
        instance.setAccessible(true);
        instance.set(instance, mock);
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

/**
 * Test method for {@link com.example.DriverSnapshotHandler#getImageURL()}.
 */
@Test
public void testFormatterServiceIsCalled() {
    DriverSnapshotHandler handler = new DriverSnapshotHandler();
    String url = handler.getImageURL();

    verify(formatter, atLeastOnce()).formatTachoIcon();
    assertEquals(MOCKED_URL, url);
}

}

Ответ 9

Насколько я знаю, класс, реализующий Singleton, не может быть расширен (конструктор суперкласса всегда называется неявным, а конструктор в Singleton является частным). Если вы хотите издеваться над классом, вы должны расширить класс. Как вы видите, в этом случае это было бы невозможно.

Ответ 10

Проблема с синглтонами (а также со статическими методами) заключается в том, что это затрудняет замену фактического кода на насмешливую реализацию.

Например, рассмотрим следующий код

public class TestMe() {
  public String foo(String data) {
    boolean isFeatureFlag = MySingletonConfig.getInstance().getFeatureFlag();
    if (isFeatureFlag)
      // do somethine with data
    else
      // do something else with the data
     return result;
  }
}

Нелегко написать единичный тест для метода foo и проверить правильность поведения. Это связано с тем, что вы не можете легко изменить возвращаемое значение getFeatureFlag.

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

Конечно, есть обходные пути, такие как powermock или инъекция зависимости к методу, или отражение в тестах. Но гораздо лучше не использовать синглтоны в первую очередь

Ответ 11

Используйте PowerMock для имитации экземпляра класса Singleton (SingletonClassHelper) и нестатического метода (nonStaticMethod), который вызывается в task.execute().

    import static org.mockito.Mockito.when;

    import org.junit.Before;
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.mockito.InjectMocks;
    import org.mockito.Mock;
    import org.mockito.Mockito;
    import org.powermock.api.mockito.PowerMockito;
    import org.powermock.core.classloader.annotations.PrepareForTest;
    import org.powermock.modules.junit4.PowerMockRunner;

    @PrepareForTest({ SingletonClassHelper.class })
    @RunWith(PowerMockRunner.class)
    public class ClassToTest {

        @InjectMocks
        Task task;

        private static final String TEST_PAYLOAD = "data";
        private SingletonClassHelper singletonClassHelper;


        @Before
        public void setUp() {
            PowerMockito.mockStatic(SingletonClassHelper.class);
            singletonClassHelper = Mockito.mock(SingletonClassHelper.class);
            when(SingletonClassHelper.getInstance()).thenReturn(singletonClassHelper);
        }

        @Test
        public void test() {
            when(singletonClassHelper.nonStaticMethod(parameterA, parameterB, ...)).thenReturn(TEST_PAYLOAD);
            task.execute();
        }
    }