Как использовать Powermockito для проверки конструкции новых объектов при тестировании метода в анонимном классе? - PullRequest
7 голосов
/ 23 сентября 2011

Мне бы хотелось написать тест JUnit, чтобы убедиться, что приведенный ниже код использует BufferedInputStream:

public static final FilterFactory BZIP2_FACTORY = new FilterFactory() {
    public InputStream makeFilter(InputStream in) {        
        // a lot of other code removed for clarity 
        BufferedInputStream buffer = new BufferedInputStream(in);
        return new CBZip2InputStream(buffer);
    }
};

(FilterFactory - это интерфейс.)

Мой тест до сих пор выглядит так:

@Test
public void testBZIP2_FactoryUsesBufferedInputStream() throws Throwable {
    InputStream in = mock(InputStream.class);
    BufferedInputStream buffer = mock(BufferedInputStream.class);
    CBZip2InputStream expected = mock(CBZip2InputStream.class);

    PowerMockito.spy(InputHelper.BZIP2_FACTORY);  // This line fails
    whenNew(BufferedInputStream.class).withArguments(in).thenReturn(buffer);
    whenNew(CBZip2InputStream.class).withArguments(buffer).thenReturn(expected);
    InputStream observed = InputHelper.BZIP2_FACTORY.makeFilter(in);

    assertEquals(expected, observed);
}

При вызове PowerMockito.spy возникает исключение с этим сообщением:

org.mockito.exceptions.base.MockitoException: 
Mockito cannot mock this class: class edu.gvsu.cis.kurmasz.io.InputHelper$1
Mockito can only mock visible & non-final classes.

Что я должен использовать вместо PowerMocktio.spy для настройки вызовов whenNew?

Ответы [ 4 ]

12 голосов
/ 27 февраля 2012

Сообщение довольно очевидно: вы не можете высмеивать невидимые и окончательные классы. Краткий ответ: Создайте именованный класс из своего анонимного и вместо этого протестируйте этот класс !

Длинный ответ, давайте копать почему!

Анонимный класс является окончательным

Вы создаете экземпляр анонимного класса FilterFactory, когда компилятор видит анонимный класс, он создает final и видимый пакет класс. Таким образом, анонимный класс не может быть смоделирован с помощью стандартного средства, то есть с помощью Mockito.

Насмешливый анонимный класс: возможно, но БРИТЛ, если не ХАКИ

Хорошо, теперь предположим, что вы хотите смоделировать этот анонимный класс с помощью Powermock. Текущие компиляторы компилируют анонимный класс по следующей схеме:

Declaring class + $ + <order of declaration starting with 1>

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

InputHelper$11.class

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

@RunWith(PowerMockRunner.class)
@PrepareForTest({InputHelper$11.class})
public class InputHelperTest {
    @Test
    public void anonymous_class_mocking works() throws Throwable {
        PowerMockito.spy(InputHelper.BZIP2_FACTORY);  // This line fails
    }
}

Этот код скомпилируется, НО в конечном итоге будет сообщено как ошибка в вашей IDE. IDE, вероятно, не знает о InputHelper$11.class. IntelliJ, который не использует скомпилированный класс для проверки отчета о коде так.

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

Так что издеваться над анонимными классами через Powermock возможно, но хрупко, никогда не делайте этого в реальном проекте!

РЕДАКТИРОВАННОЕ ПРИМЕЧАНИЕ: Компилятор Eclipse имеет другую схему нумерации, он всегда использует трехзначное число:

Declaring class + $ + <pad with 0> + <order of declaration starting with 1>

Также я не думаю, что JLS четко определяет, как компиляторы должны называть анонимные классы.

Вы не переназначаете шпиона на статическое поле

PowerMockito.spy(InputHelper.BZIP2_FACTORY);  // This line fails
whenNew(BufferedInputStream.class).withArguments(in).thenReturn(buffer);
whenNew(CBZip2InputStream.class).withArguments(buffer).thenReturn(expected);
InputStream observed = InputHelper.BZIP2_FACTORY.makeFilter(in);

PowerMockito.spy возвращает шпиона, но не меняет значение InputHelper.BZIP2_FACTORY. Таким образом, вам нужно было бы установить с помощью отражения это поле. Вы можете использовать утилиту Whitebox, которую предоставляет Powermock.

Заключение

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

Альтернативные

Я бы лучше написал следующий код:

Помощник ввода, который будет использовать именованный класс, я не использую имя интерфейса, чтобы прояснить пользователю, для чего предназначен этот фильтр!

public class InputHelper {
    public static final BufferedBZIP2FilterFactory BZIP2_FACTORY = new BufferedBZIP2FilterFactory();
}

А теперь сам фильтр:

public class BufferedBZIP2FilterFactory {
    public InputStream makeFilter(InputStream in) {
        BufferedInputStream buffer = new BufferedInputStream(in);
        return new CBZip2InputStream(buffer);
    }
}

Теперь вы можете написать такой тест:

@RunWith(PowerMockRunner.class)
public class BufferedBZIP2FilterFactoryTest {

    @Test
    @PrepareForTest({BufferedBZIP2FilterFactory.class})
    public void wraps_InputStream_in_BufferedInputStream() throws Exception {
        whenNew(CBZip2InputStream.class).withArguments(isA(BufferedInputStream.class))
                .thenReturn(Mockito.mock(CBZip2InputStream.class));

        new BufferedBZIP2FilterFactory().makeFilter(anInputStream());

        verifyNew(CBZip2InputStream.class).withArguments(isA(BufferedInputStream.class));
    }

    private ByteArrayInputStream anInputStream() {
        return new ByteArrayInputStream(new byte[10]);
    }
}

Но в конечном итоге можно было бы избежать штурма в этом тестовом сценарии, если вы заставите CBZip2InputStream принимать только BufferedInputStream. Обычно использование Powermock означает, что что-то не так с дизайном. На мой взгляд, Powermock отлично подходит для устаревших программ, но может слепить разработчиков при разработке нового кода; поскольку они упускают суть хорошей части ООП, я бы даже сказал, что они разрабатывают устаревший код.

Надеюсь, это поможет!

6 голосов
/ 09 мая 2014

Старый пост, но вам не нужно создавать именованный класс - вместо этого используйте подстановочные знаки, как упомянуто в этом посте Конструктор momo powermock с помощью whennew () не работает с анонимным классом

@PrepareForTest(fullyQualifiedNames = "com.yourpackage.containing.anonclass.*")

0 голосов
/ 25 февраля 2012

Я только что нашел ту же проблему.Таким образом, согласно документации насмешки над конструктором вам нужно подготовить класс, который создаст злой класс (ы).В вашем случае злыми классами являются BufferedInputStream и CBZip2InputStream, а их создателем является анонимный класс, который нельзя определить в аннотации PrepareForTest.Поэтому я должен был сделать то же самое, что и вы (хмм, только что увидел ваш комментарий), я переместил анонимный класс в именованный класс .

0 голосов
/ 31 октября 2011

Вам нужно запустить тест с использованием PowerMockito Runner, и вам нужно сообщить платформе, какой класс (ы) должен иметь пользовательское поведение.Добавьте следующие аннотации класса в ваш тестовый класс:

@RunWith(PowerMockRunner.class)
@PrepareForTest({ BufferedInputStream.class })
...