Синглтон и юнит-тестирование - PullRequest
35 голосов
/ 24 ноября 2011

В Effective Java есть следующее утверждение о единичных тестах

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

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

Ответы [ 10 ]

34 голосов
/ 19 января 2015

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

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

Ссылка: unit-testing-singletons

9 голосов
/ 24 ноября 2011

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

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

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

7 голосов
/ 25 ноября 2011

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

Если только вы (1) не сделаете так, чтобы синглтон реализовывал интерфейс, и (2) внедрили синглтон в ваш класс, используя этот интерфейс.

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

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

    ...
}

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

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

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

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

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

    public MyClass()
    {
    }

    ...
}

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

7 голосов
/ 25 ноября 2011

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

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

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

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

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

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

7 голосов
/ 24 ноября 2011

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

Например,

MySingleton.getInstance()

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

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

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

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

3 голосов
/ 25 ноября 2011
it’s impossible to substitute a mock implementation for a singleton

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

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

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

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

3 голосов
/ 24 ноября 2011

Синглтон объекты создаются без какого-либо контроля извне. В одной из других глав той же книги Блох предлагает использовать enum s как стандартную реализацию 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 метод.

2 голосов
/ 06 мая 2018

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

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);
}

}
0 голосов
/ 07 апреля 2018

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

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

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 илиВнедрение зависимости в метод или отражение в тестах.Но гораздо лучше не использовать синглтоны в первую очередь

0 голосов
/ 24 ноября 2011

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...