воскресенье, 21 мая 2017 г.

Методы тестирования не должны ничем делиться друг с другом

Константы ... Я писал о них некоторое время назад, в основном говоря, что они плохие, если они публичные. Они уменьшают дублирование, но вносят связь. Более эффективный способ избавиться от дублирования - создать новые классы или методы - традиционный метод ООП. 

Но одна вещь по-прежнему беспокоит меня: модульные тесты. Большинство программистов, кажется, думают, что, когда статический анализ говорит о том, что в одном файле слишком много похожих литералов, лучший способ избавиться от них - с помощью частного статического литерала. Это просто неправильно.

Модульные тесты, естественно, дублируют много кода. Методы испытаний содержат сходную или почти идентичную функциональность, и это практически неизбежно. Ну, мы можем использовать больше функций @Before и @BeforeClass, но иногда это просто невозможно. Мы можем иметь, скажем, 20 методов тестирования в одном файле FooTest.java. Подготовка всех объектов в одном «перед» невозможна. Поэтому мы должны делать определенные вещи снова и снова в наших методах тестирования.

Давайте взглянем на один из классов в нашем Takes Framework: VerboseListTest. Это модульный тест, и у него есть проблема, о которой я пытаюсь рассказать. Посмотрите на этот личный литерал MSG. Он используется в первый раз в методе setUp () в качестве аргумента конструктора объекта, а затем в нескольких тестовых методах, чтобы проверить, как этот объект ведет себя. Позвольте мне упростить этот код:

class FooTest {
  private static final String MSG = "something";
  @Before
  public final void setUp() throws Exception {
    this.foo = new Foo(FooTest.MSG);
  }
  @Test
  public void simplyWorks() throws IOException {
    assertThat(
      foo.doSomething(),
      containsString(FooTest.MSG)
    );
  }
  @Test
  public void simplyWorksAgain() throws IOException {
    assertThat(
      foo.doSomethingElse(),
      containsString(FooTest.MSG)
    );
  }
}

Это в основном то, что происходит в VerboseListTest, и это очень неправильно. Зачем? Потому что этот общий литерал MSG ввел неестественную связь между этими двумя методами тестирования. Они не имеют ничего общего, потому что они проверяют разные поведения класса Foo. Но эта частная константа связывает их вместе. Теперь они как-то связаны.

Если и когда я хочу изменить один из методов тестирования, мне может понадобиться изменить другой. Скажем, я хочу увидеть, как doSomethingElse () ведет себя, если инкапсулированное сообщение является пустой строкой. Что я делаю? Я изменяю значение константы FooTest.MSG, которое используется другим методом тестирования. Это называется сцеплением. И это плохо.

Что мы делаем? Ну, мы можем использовать этот «что-то» строковый литерал в обоих методах тестирования:

class FooTest {
  @Test
  public void simplyWorks() throws IOException {
    assertThat(
      new Foo("something").doSomething(),
      containsString("something")
    );
  }
  @Test
  public void simplyWorksAgain() throws IOException {
    assertThat(
      new Foo("something").doSomethingElse(),
      containsString("something")
    );
  }
}

Как вы видите, я избавился от этого метода setUp () и частного статического литерального MSG. Что у нас сейчас? Дублирование кода. Строка «что-то» появляется в тестовом классе четыре раза. Никакие статические анализаторы не потерпят этого. Кроме того, в VerboseListTest имеется семь (!) Тестовых методов, которые используют MSG. Таким образом, у нас будет 14 случаев «чего-то», верно? Да, это правильно, и это, скорее всего, почему один из авторов этого тестового примера представил константу - чтобы избавиться от дублирования. BTW, @ Happy-Neko сделал это в запросе № 513, @carlosmiranda рассмотрел код, и я одобрил изменения. Итак, три человека сделали / одобрили эту ошибку, включая меня.

Так что правильный подход, который позволит избежать дублирования кода и в то же время не будет вводить связь? Вот:

class FooTest {
  @Test
  public void simplyWorks() throws IOException {
    final String msg = "something";
    assertThat(
      new Foo(msg).doSomething(),
      containsString(msg)
    );
  }
  @Test
  public void simplyWorksAgain() throws IOException {
    final String msg = "something else";
    assertThat(
      new Foo(msg).doSomethingElse(),
      containsString(msg)
    );
  }
}

Эти литералы должны быть разными. Это то, о чем говорит статический анализатор, когда видит «что-то» во многих местах. Он нас спрашивает, почему они одинаковы? Неужели так важно использовать «что-то» везде? Почему вы не можете использовать разные литералы? Конечно, мы можем. И мы должны.

Суть в том, что каждый тестовый метод должен иметь свой собственный набор данных и объектов. Они никогда не должны делиться между методами тестирования. Методы испытаний всегда должны быть независимыми, не имеющими ничего общего.

Имея это в виду, мы можем легко заключить, что методы, такие как setUp () или любые общие переменные в тестовых классах, являются злыми. Они не должны использоваться и просто не должны существовать. Я думаю, что их изобретение в JUnit навредило Java-коду.


Комментариев нет:

Отправить комментарий