JMockit: класс Singleton, порядок тестирования

У меня есть класс Singleton для тестирования:

public class Singleton {
    private static Singleton instance;

    private List<String> list;

    private Singleton() {

    }

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                instance = new Singleton();
            }
        }
        return instance;
    }

    public boolean methodOne() {
        if (list == null) {
            list = new ArrayList<String>();
            list = SomeClass.fillListOne();
        }
        return SomeClass.verifyList(list);
    }

    public boolean methodTwo() {
        if (list == null) {
            list = new ArrayList<String>();
            list = SomeClass.fillListTwo();
        }
        return SomeClass.verifyList(list);
    }
}

Со следующим тестовым классом:

@RunWith(JMockit.class)
public class SingletonTest {
    @Test
    public void testOne(final @Mocked SomeClass someClass) {
        Singleton.getInstance().methodOne();
        new Verifications() {
            {
                SomeClass.fillListOne();
            }
        };

    }

    @Test
    public void testTwo(final @Mocked SomeClass someClass) {
        Singleton.getInstance().methodTwo();
        new Verifications() {
            {
                SomeClass.fillListTwo();
            }
        };
    }
}

Если я выполняю только «testOne» или только «testTwo», тесты проходят. Если я выполняю все тесты, он проходит только первый выполненный метод. Как я могу установить для атрибута «список» значение null, например, в методе @Before? Как использовать деинкапсуляцию с синглтоном или частным членом без сеттеров?


person borras    schedule 30.10.2014    source источник
comment
К вашему сведению: этот метод getInstance невероятно небезопасен. Вы не можете синхронизироваться внутри нулевой проверки и ожидать, что это будет безопасно. Поток A входит и проверяет, является ли экземпляр нулевым, это не так, поэтому он продолжается. Переключатель контекста, поток B приходит, чтобы проверить, является ли экземпляр нулевым, но это не так, поэтому он продолжается. Поток B получает блокировку и инициализирует некоторый объект, назначает его экземпляру (который гарантированно будет назначен и инициализирован в соответствии с требованиями синхронизированного) и освобождает блокировку. Переключение контекста, поток A теперь получает блокировку и делает то же самое, перезаписывая экземпляр из ThreadB.   -  person searchengine27    schedule 23.07.2015
comment
Рекомендуемое чтение для начинающих разработчиков параллелизма Java: cs.umd.edu/~ pugh/java/memoryModel/DoubleCheckedLocking.html (вы сделали самую первую ошибку, описанную здесь)   -  person searchengine27    schedule 23.07.2015
comment
Уточнение: (который гарантированно будет назначен и инициализирован в соответствии с требованиями synchronized), означает, что к моменту снятия блокировки объект гарантированно будет назначен и инициализирован. Согласно JMM, порядок, в котором это происходит внутри синхронизированного блока, недетерминирован. Чтобы уточнить, ваш пример действительно становится жертвой этого, но также становится жертвой моего первоначального примера (который не имеет отношения к JMM, но знание того, как JMM справляется с этим, облегчит ваше «решение» этой проблемы, которую я опубликовал подробности, почему это не сработает).   -  person searchengine27    schedule 23.07.2015
comment
Спасибо searchengine27, но вопрос был не про синглтон и синхронизацию. Как я уже писал в предыдущем комментарии, я не могу редактировать исходный класс.   -  person borras    schedule 27.07.2015


Ответы (2)


Этот класс не особенно восприимчив к тестированию. Я призываю вас переписать его, если у вас есть такая возможность. (Тестирование с имитацией статических вызовов методов — это кошмар обслуживания, и я также испытываю общую неприязнь ко всем вещам singleton-y — кажется, они используются везде, независимо от того, должны они быть или нет).

Несмотря на это, документация предлагает вы можете сделать что-то вроде этого (я также добавил дополнительное поле для удобства чтения, хотя вам это не обязательно):

@RunWith(JMockit.class)
public class SingletonTest {
    private Singleton instance;

    @Before
    public void initialise() {
        Deencapsulation.setField(Singleton.class, "instance", null);
        instance = Singleton.getInstance();
    }

    @Test
    public void testOne(final @Mocked SomeClass someClass) {
        instance.methodOne();
        new Verifications() {
            {
                SomeClass.fillListOne();
            }
        };
    }
    // ...other tests...
}
person blgt    schedule 30.10.2014
comment
СПАСИБО! Класс, который я написал, является упрощением более сложного класса, который я не могу редактировать. Я не хочу обнулять экземпляр, только список. Поэтому я редактирую ваш метод инициализации:instance = Singleton.getInstance(); Deencapsulation.setField (экземпляр, список, ноль); - person borras; 30.10.2014
comment
@borras Да, был там, вот где неприятные размышления, подобные приведенным выше, становятся полезными. Что касается сброса list - ваш тест будет более надежным, если вы сбросите весь instance, а не только его часть (я предполагаю, что текущие ресурсы не являются проблемой) - person blgt; 30.10.2014

Я обнаружил проблему с существующим ответом: даже когда методы SomeClass не вызываются, тест проходит. Это противоречит цели теста: он должен пройти тогда и только тогда, когда вызываются методы SomeClass.fillListOne() и SomeClass.fillListTwo().

Вот способ проверить вызовы с помощью JMockit, используя NonStrictExpectations для зависимости SomeClass, чтобы вернуть значение null. Я протестировал его, используя тот же пример, указанный в вопросе, он терпит неудачу, когда вызовы не выполняются, и проходит, когда они есть:

@RunWith(JMockit.class)
public class SingletonTest {
    // capturing ensures Jmockit will mock all SomeClass instances.
    @Capturing
    private SomeClass someClass;

    @Before
    public void prepareTests() {
        new NonStrictExpectations() {{
            SomeClass.fillListOne(); result = null;
            SomeClass.fillListTwo(); result = null;
        }};
    }

    @Test
    public void testOne() {
        Singleton.getInstance().methodOne();
        new Verifications() {{
            SomeClass.fillListOne();
        }};
    }

    @Test
    public void testTwo() {
        Singleton.getInstance().methodTwo();
        new Verifications() {{
            SomeClass.fillListTwo();
        }};
    }
}
person Tarek    schedule 22.11.2015