Тестирование слабых ссылок

Каков правильный подход к тестированию слабой ссылки в Java?

Моя первоначальная идея состоит в том, чтобы сделать следующее:

public class WeakReferenceTest {

    public class Target{
        private String value;    

        public Target(String value){
            this.value = value;
        }    
        public String toString(){
            return value;
        }
    }

    public class UsesWeakReference{    
        WeakReference<Target> reference;   

        public UsesWeakReference(Target test){
            reference = new WeakReference<Target>(test);
        }    
        public String call(){
            Target test = reference.get();
            if(test != null){
                return test.toString();
            }
            return "empty";
        }
    }

    @Test
    public void testWeakReference(){    
        Target target = new Target("42");

        UsesWeakReference usesWeakReference = new UsesWeakReference(target);    
        WeakReference<Target> triggerReference = new WeakReference<Target>(target);    
        assertEquals("42", usesWeakReference.call());

        target = null;    
        while(triggerReference.get() != null){
            System.gc();
        }

        assertEquals("empty", usesWeakReference.call());    
    }    
}

У меня есть оговорка по поводу подхода с использованием System.gc(), поскольку я понимаю, что он может вести себя по-разному на разных JVM.


person John Ericksen    schedule 24.06.2012    source источник


Ответы (4)


Не существует 100% надежного способа тестирования кода, использующего эталонные типы. Поведение эталонных объектов зависит от того, когда запускается сборщик мусора, и не существует стопроцентно надежного способа принудительного запуска сборщика мусора.

Лучшее, что вы можете сделать, это:

  • убедитесь, что у вас установлены правильные параметры JVM при запуске тестов, и
  • напишите свой тест так, чтобы он не давал сбоев в случае, если System.gc() не работает, ИЛИ был готов отключить или пропустить тест, или проигнорировать сбой теста.

(Вы должны быть в состоянии обнаружить, что System.gc() игнорируется, посмотрев, сколько памяти используется до и после вызова, например, вызвав Runtime.totalMemory())


На самом деле есть еще одно «решение». Пусть ваш модульный тест генерирует огромное количество мусора ... достаточно, чтобы гарантировать, что вы запустите сборку мусора. (Не очень хорошая идея, имхо.)

person Stephen C    schedule 24.06.2012

Новый ответ на старый вопрос; Я нашел ваш вопрос, поскольку имею дело с той же проблемой: я хочу написать модульный тест, чтобы убедиться, что мой тестируемый класс делает что-то очень конкретное, если референт WeakReference становится нулевым.

Сначала я написал простой тестовый пример, который устанавливает референт в значение null; чтобы затем позвонить System.gc(); и что интересно: по крайней мере, в моем затмении этого было «достаточно хорошо», чтобы мой weakRefernce.get() возвращал значение null.

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

Итак, подумав еще немного:

@Test
public void testDeregisterOnNullReferentWithMock() {
    @SuppressWarnings("unchecked")
    WeakReference<Object> weakReference = EasyMock.createStrictMock(WeakReference.class);
    EasyMock.expect(weakReference.get()).andReturn(null);
    EasyMock.replay(weakReference);
    assertThat(weakReference.get(), nullValue());
    EasyMock.verify(weakReference);
}

Тоже хорошо работает.

Значение: общий ответ на эту проблему — фабрика, которая создает для вас WeakReference для объектов. Итак, когда вы хотите протестировать свой производственный код; вы предоставляете ему издевательскую фабрику; и эта фабрика, в свою очередь, будет издеваться над объектами WeakReference; и теперь вы полностью контролируете поведение этого слабого эталонного объекта.

И «полный контроль» намного лучше, чем предполагать, что GC, возможно, делает то, на что вы надеетесь.

person GhostCat    schedule 28.04.2016
comment
Здесь лучше всего использовать насмешки, поскольку сборщик мусора не является абсолютно детерминированным и управляемым. - person kerner1000; 26.07.2017
comment
Очень правильно — я добавил еще один абзац, чтобы было понятнее. Для протокола: есть ли что-нибудь еще, что я мог бы сделать, чтобы сделать этот голос достойным в ваших глазах? - person GhostCat; 26.07.2017
comment
Я думаю, это уже достаточно ясно. Мне пришлось создать еще один конструктор для моего типа, который я хочу протестировать, чтобы предоставить имитацию WeakReference (конструктор в первую очередь предназначен для тестирования чистого кода? ;)). Возможно, следует отметить, что имитация WeakReference лучше всего подходит для воспроизводимых тестов, но менее реалистична, а это означает, что если вы хотите явно протестировать ненадежное поведение сборщика мусора, это, возможно, не лучший подход. (спасибо за плюс ;)) - person kerner1000; 26.07.2017
comment
Хорошо. Вы можете использовать другой конструктор (который я бы сделал защищенным пакетом); или при использовании Mockito есть аннотация @InjectMocks (которую я лично не использую, так как она молча терпит неудачу - так что вы никогда не знаете, действительно ли ваши макеты были введены). - person GhostCat; 26.07.2017

Я хочу, чтобы GhostCat приветствовал ответ Моники С, в котором говорится об использовании насмешек. Это, безусловно, один маршрут, однако при его реализации я заметил, что на самом деле есть функция clear() на самом WeakReference. Таким образом, вместо того, чтобы выполнять тяжелую работу по созданию макетов, вы можете создать экземпляр фабрики и просто очистить референт самостоятельно. Подход, который я использовал, написан на Kotlin, поэтому, надеюсь, синтаксис не слишком раздражает, но это именно то, что у меня есть.

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

interface WeakReferenceFactory {
    fun <T> create(referent: T): WeakReference<T>

    companion object {
        // Allows us to create a production ready instance with WeakReferenceFactory(), avoids having to implement a concrete instance.
        operator fun invoke(): WeakReferenceFactory {
            return object : WeakReferenceFactory {
                override fun <T> create(referent: T): WeakReference<T> {
                    return WeakReference(referent)
                }
            }
        }
    }
}

Затем для наших тестов мы можем реализовать фабрику с дополнительной функцией clear(), что позволит нам сохранить ссылку на экземпляр, который мы используем в тесте, а затем просто передать его в фабрику для очистки.

class WeakReferenceFactoryFake : WeakReferenceFactory {
    private val managedReferents = mutableListOf<WeakReference<*>>()

    fun <T> clear(referent: T) {
        managedReferents.filter { it.get() == referent }
            .forEach { it.clear() }
    }

    override fun <T> create(referent: T): WeakReference<T> {
        val weakReference = WeakReference(referent)
        managedReferents.add(weakReference)
        return weakReference
    }
}

Тогда в вашем тесте у вас будет что-то вроде этого (извините за Foo's и Bar's).

class FooTest {
    private val fakeWeakReferenceFactory = WeakReferenceFactoryFake()

    private val subject: Foo = Foo(fakeWeakReferenceFactory)

    @Test
    fun `given foo, when bar is cleared, then bar should be null`() {
        val bar = Bar()
        foo.put(bar)

        fakeWeakReferenceFactory.clear(bar)

        assert(bar).isNull()
    }
}
person CodyEngel    schedule 29.04.2020

Сегодня столкнулся с похожей проблемой.

Решение TLDR: расширить WeakReference и переопределить .get()


Пытался использовать mockk для имитации WeakReference<*> следующим образом:

val weakRef = mockk<WeakReference<String>>()

@Test
fun testWeakRef() {
   every {weakRef.get()} returns "apple"
   // ... etc, etc
}

Однако затем я получаю

Missing mocked calls inside every { ... } block: make sure the object inside the block is a mock

Пока не уверен, почему mockk не любит издеваться над WeakReference.

Поэтому вместо этого просто расширьте класс в вашем тестовом каталоге.

class MockWeakReference<T>(initialValue: T? = null) : WeakReference<T>(null) {

    private var mockValue = initialValue

    override fun get(): T? {
        return mockValue
    }

    fun setMockValue(value: T?) {
        mockValue = value
    }
}

а затем вместо использования every или when просто используйте mockWeakRef.setMockValue($x)

person Jeff Padgett    schedule 12.07.2021