Статьи

Пересмешивающие статические методы в Android: давайте подведем итоги

При написании локальных модульных тестов в Android одно из ограничений, с которыми вы сталкиваетесь, заключается в том, что тесты запускаются для версии android.jar, в которой нет кода. Как объясняется в документации , любая зависимость от кода Android должна быть проверена.

Быстрый пример простого юнит-теста:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class ClassUnderTest {
 
public String methodUnderTest(String str)
{
    if (PhoneNumberUtils.isGlobalPhoneNumber(str))
    {
      return "yes";
    }
    else
    {
      return "no";
    }
  }
}
 
@RunWith(JUnit4.class)
public class TestThatFails {
 
private ClassUnderTest classUnderTest;
 
  @Before
  public void setup() {
    classUnderTest = new ClassUnderTest();
  }
 
  @Test
  public void testTheClass() {
    String result = classUnderTest.methodUnderTest("1234");
    assertEquals("yes", result);
  }
}

Когда этот тест запущен, он потерпит неудачу со следующей ошибкой:

1
java.lang.RuntimeException: Method isGlobalPhoneNumber in android.telephony.PhoneNumberUtils not mocked. See http://g.co/androidstudio/not-mocked for details

Класс, который мы тестируем, зависит от библиотеки утилит Android PhoneNumberUtils . Для успешного выполнения теста необходимо проверить зависимость Android.

Весь пример кода для этого поста доступен в этой сути .

Мокито: нет статическим методам

Предложенный Google способ смоделировать зависимости Android — использовать Mockito . В целом это было бы хорошо, однако в нашем примере это не будет работать, потому что Mockito не поддерживает насмешливые статические методы.

Это обсуждение показывает, что участники Mockito считают статические методы анти-паттернами по разным причинам, например

  • Зависимость от статического метода становится жесткой в ​​коде.
  • Это затрудняет издевательство и тестирование.

Следовательно, они не поддерживают это, поскольку они не хотят поощрять плохой дизайн.

Итак, как еще можно заставить наш тест работать?

  • Если бы это была простая старая Java вместо Android, я мог бы использовать PowerMockito, чтобы высмеивать статические методы. Однако я обнаружил, что использование PowerMock проблематично в Android.
  • Если вы используете только несколько статических методов, вы можете просто скопировать код в свое приложение, предполагая, что источник доступен. Конечно, это означает, что нужно поддерживать больше кода, и оно не является устойчивым, если вы используете много статических методов.
  • Вы можете заключить вызов статического метода и внутренне делегировать статический метод. Обертка может быть затем издевались. Это вариант, который мы будем обсуждать.

Классы Обертки

Одним из решений является создание класса-оболочки для классов Android, имеющих статический метод, и добавление этой оболочки в качестве зависимости.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class PhoneNumberUtilsWrapper {
 
  public boolean isGlobalPhoneNumber(String phoneNumber)
  {
    return PhoneNumberUtils.isGlobalPhoneNumber(phoneNumber);
  }
}
 
public class ClassUnderTestWithWrapper {
 
  private PhoneNumberUtilsWrapper wrapper;
 
  public ClassUnderTestWithWrapper(PhoneNumberUtilsWrapper wrapper) {
    this.wrapper = wrapper;
  }
 
  public String methodUnderTest(String str)
  {
    if (wrapper.isGlobalPhoneNumber(str))
    {
      return "yes";
    }
    else
    {
      return "no";
    }
  }
}

Здесь я создал класс-оболочку для PhoneNumberUtils, который теперь является зависимостью от тестируемого класса.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@RunWith(JUnit4.class)
public class TestWithWrapper {
 
  @Mock
  PhoneNumberUtilsWrapper wrapper;
 
  private ClassUnderTestWithWrapper classUnderTest;
 
  @Before
  public void setup() {
    MockitoAnnotations.initMocks(this);
 
    classUnderTest = new ClassUnderTestWithWrapper(wrapper);
  }
 
  @Test
  public void testTheClass() {
    when(wrapper.isGlobalPhoneNumber(anyString())).thenReturn(true);
 
    String result = classUnderTest.methodUnderTest("1234");
    assertEquals("yes", result);
  }
}

Поскольку класс-обертка может быть смоделирован, а вызов метода в тестируемом классе не является статическим, тест теперь можно пройти.

Одна из проблем этого решения заключается в том, что тестируемый класс зависит от статических методов из многих библиотек Android. Например, что произойдет, если тестируемый класс также должен будет использовать TextUtils , DateUtils и т. Д. Внезапно вы получите гораздо больше стандартного кода, больше параметров конструктора и т. Д.

Методы Обертки

Другой способ — обернуть вызов статического метода в нестатический метод в тестируемом классе.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
public class ClassUnderTestWithWrappedMethod {
 
  public String methodUnderTest(String str)
  {
    if (isGlobalPhoneNumber(str))
    {
      return "yes";
    }
    else
    {
      return "no";
    }
  }
 
  // can't be private access
  boolean isGlobalPhoneNumber(String phoneNumber)
  {
    return PhoneNumberUtils.isGlobalPhoneNumber(phoneNumber);
  }
}

Чтобы это работало, в тесте мы должны использовать шпион Mockito . Также обратите внимание, что упакованные методы должны быть доступны в тесте и поэтому не могут быть приватными.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@RunWith(JUnit4.class)
public class TestWithWrappedMethod {
 
  private ClassUnderTestWithWrappedMethod classUnderTest;
 
  private ClassUnderTestWithWrappedMethod classUnderTestSpy;
 
  @Before
  public void setup() {
    MockitoAnnotations.initMocks(this);
 
    classUnderTest = new ClassUnderTestWithWrappedMethod();
    classUnderTestSpy = Mockito.spy(classUnderTest);
  }
 
  @Test
  public void testTheClass() {
    doReturn(true).when(classUnderTestSpy)
    .isGlobalPhoneNumber(anyString());
 
    String result = classUnderTestSpy.methodUnderTest("1234");
    assertEquals("yes", result);
  }
}

Здесь мы запускаем тест для класса spy, который делегирует вызовы методов тестируемому реальному классу. Однако мы можем создать заглушки для методов, которые переносят статические вызовы методов в библиотеки Android.

Как я уже упоминал, одним из недостатков является то, что упакованные методы не могут быть закрытыми, что не является идеальным с точки зрения разработки ОО. Но тогда вы должны пойти на подобные компромиссы, если вы используете такие библиотеки, как Dagger или Butterknife .

Вывод

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

Статические методы: хорошо или плохо? Это имеет значение?

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

Однако в мире Java они являются фактом жизни.

Многие служебные классы в Java, Android и многие популярные библиотеки — это не настоящие ОО-классы, а наборы процедурных функций. Функции часто пишутся как статические методы и группируются по функциональности.

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

Опубликовано на Java Code Geeks с разрешения Дэвида Вонга, партнера нашей программы JCG . Смотрите оригинальную статью здесь: Пересмешивающие статические методы в Android: давайте подведем итоги

Мнения, высказанные участниками Java Code Geeks, являются их собственными.