Отслеживание состояния локального объекта и прерывания потока с помощью (Power) Mockito? - PullRequest
0 голосов
/ 21 декабря 2018

Я тестирую микросервис Spring Boot.Я хочу использовать Mockito, чтобы вызвать исключение при вызове метода в определенном процессе, чтобы я мог улучшить покрытие модульных тестов.Проблема в том, что этот метод (назовем его doB()) вызывается объектом, который существует только в локальной области действия метода (B), созданным статическим конечным объектом фабрики (A).

Я хочу сделать что-то вроде:

doThrow(new RuntimeException()).when(mockedB).doB(anyString());

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

Но возможно ли вообще создать mockedB полезным способом?Если да, то как?

Я пробовал различные комбинации mock() и spy(), правда, с небольшим успехом.Я новичок в использовании Mockito, но я почти уверен, что суть проблемы в том, что если я просто высмею Foo, то я не смогу видеть A и B внутри, что-то делать, но пытатьсяложный или шпионский A или B не работает, так как они не совпадают с A или B, созданными внутри Foo.A, будучи финальным и статичным, вероятно, и здесь не дает мне никакой пользы.

В качестве примера я удалил почти все, кроме самой необходимой функциональности.Класс, который я тестирую, это Foo, который использует A и B для внутреннего использования.

Вот Foo:

public class Foo {
    private static final A localA = new A();

    public FooResult doFoo(String fooString) {
        try {
            B localB = localA.createB();
            return localB.doB(fooString);
        } catch (RuntimeException e) {
            //exception handling here
        }
        return null;
    }

}

И это A:

public class A {

    //unimportant internal details
    private Object property;

    public A() {
        this(null);
    }

    public A(Object property) {
        this.property = property;
    }

    public B createB() {
        //assume for sake of example that this constructor 
        //is not easily accessible to classes other than A
        return new B();
    }
}

А теперь B:

public class B {    

    public FooResult doB(String str) throws RuntimeException {

        //lots of processing, yada yada...
        //assume this exception is difficult to trigger just
        //from input due to details out of our control
        return new FooResult(str);
    }
}

Вот FooResult

public class FooResult {
    private String fooString;
    public FooResult(String str) {
        this.fooString = str;
    }
    public String getFooString() {
        return fooString;
    }
}

Наконец, вот тест:

@RunWith(PowerMockRunner.class)
public class FooTest {

    @InjectMocks
    Foo foo;

    @Test
    public void testDoFoo() {
        String fooString = "Hello Foo";
        FooResult fooResult = foo.doFoo(fooString);
        assertEquals(fooResult.getFooString(), fooString);
        //works fine, nothing special here
    }

  @Test
  @PrepareForTest({Foo.class, A.class, B.class})
  public void testDoFooException() throws Exception {

    //magic goes here???
    A mockedA = PowerMockito.mock(A.class);
    B mockedB = PowerMockito.mock(B.class);

    PowerMockito.when(mockedA.createB()).thenReturn(mockedB);
    PowerMockito.doThrow(new RuntimeException()).when(mockedB).doB(Mockito.anyString());

    FooResult fooResult = foo.doFoo("Hello Foo");

    //expect doFoo() to fail and return null
    assertNull(fooResult);
  }


}

Как я уже говорил ранее, я ожидаю, что макет сработает при вызове doB(), в результате чего doB() вернет null.Это не работает, и исключение не выбрасывается.

У меня такое чувство, что пытаться делать это - плохая практика.Возможно, лучшим способом было бы изменить метод так, чтобы я мог вместо этого передать свой собственный A объект, чтобы я мог наблюдать его.Но давайте просто скажем, что я не могу изменить любой исходный код.Это вообще возможно?

1 Ответ

0 голосов
/ 21 декабря 2018

Я только что нашел решение, когда набирал это, так что я решил пойти дальше и поделиться им.Yay!

Кредит идет к этому посту за ответ:

Так как Mocking не может обработать final, вместо этого мы в итоге взломали кореньсамо поле.Когда мы используем манипуляции с полем (отражение), мы ищем конкретную переменную внутри класса / объекта.Как только Java находит его, мы получаем его «модификаторы», которые сообщают переменной, какие у нее ограничения / правила, такие как final, static, private, public и т. Д. Мы находим правильную переменную, а затем сообщаем коду, что он доступен, которыйпозволяет нам изменить эти модификаторы.Как только мы изменили «доступ» в корне, чтобы позволить нам манипулировать им, мы отключаем «последнюю» его часть.Затем мы можем изменить значение и установить его так, как нам нужно.

Используя их функцию setFinalStatic, я могу успешно смоделировать A и вызвать выброс RuntimeException.

Вот рабочий тест вместе с помощником:

//make fields accessible for testing
private static void setFinalStatic(Field field, Object newValue) throws Exception {
      field.setAccessible(true);
      // remove final modifier from field
      Field modifiersField = Field.class.getDeclaredField("modifiers");
      modifiersField.setAccessible(true);
      modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
      field.set(null, newValue);
}   

@Test
@PrepareForTest({A.class})
public void testDoFooException() {
  A mockedA = PowerMockito.mock(A.class);
  B mockedB = PowerMockito.mock(B.class);

  try {
    setFinalStatic(Foo.class.getDeclaredField("localA"), mockedA);
  } catch (Exception e) {
    fail("setFinalStatic threw exception: " + e);
  }

  Mockito.when(mockedA.createB()).thenReturn(mockedB);
  PowerMockito.doThrow(new RuntimeException()).when(mockedB).doB(Mockito.anyString());

  FooResult fooResult = foo.doFoo("Hello Foo");
  assertNull(fooResult);
}

Конечно, если у кого-то есть лучший и менее хакерский способ сделать это, я был бы рад принять этот ответ вместо этого.

...