Как издеваться над статическим синглтоном?

У меня есть несколько классов, в которых меня попросили добавить несколько модульных тестов с помощью Rhino Mocks, и у меня возникли некоторые проблемы.

Во-первых, я знаю, что RhinoMocks не позволяет насмехаться над статическими членами. Ищу какие варианты есть (кроме TypeMock).

Пример моего класса аналогичен приведенному ниже:

class Example1 : ISomeInterface
{
    private static ISomeInterface _instance;

    private Example1()
    {
        // set properties via private static methods
    }

    static Example1()
    {
        _instance = new Example1();
    }

    public static ISomeInterface Instance() 
    {
        get { return _instance; }
    }

    // Instance properties 

    // Other Instance Properties that represent objects that follow a similar pattern.
}

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

Example1.Instance.SomeObject.GoDownARabbitHole();

Есть ли у меня способ высмеять SomeObject.GoDownARabbitHole() в этой ситуации или высмеять Экземпляр?


person JamesEggers    schedule 12.01.2010    source источник
comment
Вы пробовали Moq вместо Rhino? Я считаю, что это позволит вам издеваться над статическими методами. См. superexpert.com/blog/archive / 2008/06/12 / и раздел с шаблоном адаптера для имитации статического метода.   -  person Lucas B    schedule 12.01.2010
comment
К сожалению, решение изменить фреймворк mocking не находится в моих руках. У меня огромная база кода, и он стандартизирован для Rhino. Что касается использования шаблона адаптера для имитации статического метода, то же самое можно сделать и с Rhino. Проблема, с которой я столкнулся, больше связана с созданием синглтона с помощью статического метода, чем с тестированием самого статического метода.   -  person JamesEggers    schedule 12.01.2010
comment
Moq не может имитировать статические методы, не следуя шаблону адаптера. Я должен добавить, что он намного лучше, чем Rhino и другие фреймворки для фиксации.   -  person Finglas    schedule 12.01.2010


Ответы (7)


Синглтоны не подходят для тестирования, потому что их очень сложно изменить. Было бы намного лучше использовать Внедрение зависимостей для внедрения экземпляра ISomeInterface в ваши классы потребления:

public class MyClass
{
    private readonly ISomeInterface dependency;

    public MyClass(ISomeInterface dependency)
    {
        if(dependency == null)
        {
            throw new ArgumentNullException("dependency");
        }

        this.dependency = dependency;
    }

    // use this.dependency in other members
}

Обратите внимание, как Guard Claus вместе с ключевым словом readonly гарантирует, что экземпляр ISomeInterface всегда будет доступен.

Это позволит вам использовать Rhino Mocks или другую динамическую имитационную библиотеку для внедрения тестовых двойников ISomeInterface в потребляющие классы.

person Mark Seemann    schedule 12.01.2010
comment
Внедрение зависимостей иногда может значительно увеличить сложность кода. Представьте, что у вас есть внешняя зависимость, к которой должен быть легко получить доступ из любого места в вашем коде. static instance - лучший выбор, потому что в противном случае вы просто загрязните все свои конструкторы. - person Konrad; 14.12.2018
comment
Одна из таких зависимостей - локализатор, который должен переводить ваши строки. Очевидно, он получит доступ к какому-либо файлу или базе данных для загрузки переводов, и вам придется имитировать это. Имитировать статические экземпляры невозможно. И воткнуть локализатор в каждый класс просто уродство! - person Konrad; 14.12.2018
comment
stackoverflow.com/questions/46801776/ - person Konrad; 14.12.2018

Обескураженный подобными темами, мне потребовалось некоторое время, чтобы заметить, что синглтоны не так уж и сложно высмеять. В конце концов, почему мы используем C #?

Просто используйте Reflection.

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

ISomeInterface unused = Singleton.Instance();

System.Reflection.FieldInfo instance = typeof(Example1).GetField("_instance", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic);

Mock<ISomeInterface> mockSingleton = new Mock<ISomeInterface>();
instance.SetValue(null, mockSingleton.Object);

Я предоставил код для насмешек с Moq, но я думаю, что Rhino Mocks очень похож.

person No answer    schedule 19.08.2013
comment
просто использовал это на работе и вернул кого-то на рельсы. Спасибо. - person JJS; 17.10.2013
comment
Проблема в том, что большинство синглтонов не основано на интерфейсах. Я бы сказал, это довольно редко. Если у кого-то есть интерфейс, лучше знать, что использовать не синглтоны, а внедрение зависимостей. - person andrew.fox; 09.03.2015
comment
вау, это решение заставляет реорганизовать мои синглтоны для реализации интерфейсов. - person silver; 20.07.2015
comment
Один элемент для добавления. После того, как он был смоделирован, другие ваши тесты также будут использовать этот макет. Вы можете этого не хотеть. В конце теста вы должны удалить макет, используя: instance.SetValue (_instance, null); - person Roger; 25.09.2018

Вот подход с низким уровнем взаимодействия, который использует делегат, который может быть установлен изначально и изменен во время выполнения. Лучше пояснить это на примере (в частности, насмехаясь над DateTime.Now):

http://www.lostechies.com/blogs/jimmy_bogard/archive/2008/11/09/systemtime-versus-isystemclock-dependencies-revisited.aspx.

person Peter Seale    schedule 12.01.2010
comment
Очень интересный подход к выкройке адаптера. Спасибо - person JamesEggers; 12.01.2010

Пример из книги: Эффективная работа с Устаревший код

Чтобы запустить код, содержащий синглтоны в тестовой обвязке, мы должны ослабить свойство singleton. Вот как мы это делаем. Первый шаг - добавить новый статический метод в одноэлементный класс. Метод позволяет заменить статический экземпляр в синглтоне. Мы назовем его setTestingInstance.

public class PermitRepository
{
    private static PermitRepository instance = null;
    private PermitRepository() {}
    public static void setTestingInstance(PermitRepository newInstance)
    {
        instance = newInstance;
    }
    public static PermitRepository getInstance()
    {
        if (instance == null) 
        {
            instance = new PermitRepository();
        }
        return instance;
    }
    public Permit findAssociatedPermit(PermitNotice notice) 
    {
    ...
    }
...
}

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

public void setUp() {
PermitRepository repository = new PermitRepository();
...
// add permits to the repository here
...
PermitRepository.setTestingInstance(repository);
}
person Teoman shipahi    schedule 19.02.2015
comment
Мужик, мне нравится твое предложение по двум причинам: простое и реалистичное. Сегодня изучаю, потому что у меня начинаются проблемы с одиночками при выполнении тестов. Я реализовал решение, аналогичное тому, которое вы поставили, даже не прочитав книгу (она в моем списке для чтения). Мне было неудобно, потому что большинство ответов и статей, которые я нашел, давали ответы, которые в лучшем случае упрощены (по крайней мере, для корпоративного приложения с 60 КБ строк кода). Теперь мне удобнее знать, что кто-то уважаемый написал об этом в книге. В конце концов, это предложил Майкл Фезерс. - person Marlon Patrick; 12.04.2015
comment
@Teoman shipahi: Мне нравится эта идея, но как вы можете вызвать new PermitRepository() в своем тесте, если конструктор private (что очень часто бывает для синглтонов)? - person György Balássy; 03.04.2018
comment
@ GyögeryBalássy Edit: я считаю, что автор, намеревавшийся использовать PermitRepository в настройке, сам по себе является фиктивным классом. books.google.com/ - person Teoman shipahi; 03.04.2018

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

Например:

public class UseTheSingleton
{
    private ISomeInterface myX;

    public UseTheSingleton(ISomeInterface x)
    {
        myX = x;
    }

    public void SomeMethod()
    {
        myX.
    }
}

Потом ...

UseTheSingleton useIt = UseTheSingleton(Example1.Instance);
person TheSean    schedule 12.01.2010

Ознакомьтесь с инъекцией зависимостей.

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

За советами по модульному тестированию и дальнейшими проблемами тестирования обращайтесь к блогу тестирования Google, в частности к статьям Misko.

Пример

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

person Finglas    schedule 12.01.2010
comment
Строка примера использования, которую я привел под определением класса, используется, вероятно, примерно в 100 или более файлах. На самом деле я пытаюсь протестировать класс, который вызывает этот статический одноэлементный класс, что было бы просто, если бы я мог издеваться над этим. Таким образом, рабочая нагрузка по превращению его в класс экземпляра, а не в статический синглтон, к сожалению, была бы рискованной. - person JamesEggers; 12.01.2010
comment
Это достаточно честно. В этом случае DI будет лучшим вариантом. Удачного тестирования. - person Finglas; 12.01.2010

Вам не нужно исправлять все использования сразу, только то, с которым вы имеете дело сейчас. Добавьте поле ISomeInterface к тестируемому классу и установите его через конструктор. Если вы используете Resharper (вы используете Resharper, не так ли?), Большую часть этого будет нетрудно сделать. Если это действительно неудобно, у вас может быть несколько конструкторов, один из которых устанавливает новое поле зависимости, а другой вызывает первый с синглтоном в качестве значения по умолчанию.

person Steve Freeman    schedule 30.10.2010