Подтвердить что ты не робот

Твердая Java unit test автоматизация? (JUnit/Hamcrest/...)

Намерение

Я ищу следующее:

  • Твердая модульная методика тестирования
    • Что мне не хватает в моем подходе?
    • Что я делаю неправильно?
    • Что я делаю, что не нужно?
  • Способ получения максимально возможного значения автоматически

Текущая среда

  • Eclipse как IDE
  • JUnit как среда тестирования, интегрированная в Eclipse
  • Hamcrest как библиотека "matchers", для лучшей читаемости утверждения
  • Google Guava для проверки предварительных условий

Текущий подход

Структура

  • Один тестовый класс для каждого класса для тестирования
  • Тестирование методов сгруппировано в статические вложенные классы
  • Именование метода метода для определения тестируемого поведения + ожидаемый результат
  • Ожидаемые исключения, указанные Java Annotation, а не в имени метода

Методология

  • Следите за значениями null
  • Следите за пустым List <E>
  • Следите за пустым String
  • Остерегайтесь пустых массивов
  • Остерегайтесь инвариантов состояния объекта, измененных кодом (пост-условия)
  • Способы принимают документированные типы параметров
  • Граничные проверки (например, Integer.MAX_VALUE и т.д.)
  • Документирование неизменяемости по определенным типам (например, Google Guava ImmutableList <E> )
  • ... есть ли список для этого? Примеры хороших тестовых списков:
    • Вещи для проверки проектов базы данных (например, CRUD, подключение, ведение журнала,...)
    • Что нужно проверить в многопоточном коде.
    • Что нужно проверить для EJB
    • ...?

Пример кода

Это надуманный пример, чтобы показать некоторые методы.


MyPath.java

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import java.util.Arrays;
import com.google.common.collect.ImmutableList;
public class MyPath {
  public static final MyPath ROOT = MyPath.ofComponents("ROOT");
  public static final String SEPARATOR = "/";
  public static MyPath ofComponents(String... components) {
    checkNotNull(components);
    checkArgument(components.length > 0);
    checkArgument(!Arrays.asList(components).contains(""));
    return new MyPath(components);
  }
  private final ImmutableList<String> components;
  private MyPath(String[] components) {
    this.components = ImmutableList.copyOf(components);
  }
  public ImmutableList<String> getComponents() {
    return components;
  }
  @Override
  public String toString() {
    StringBuilder stringBuilder = new StringBuilder();
    for (String pathComponent : components) {
      stringBuilder.append("/" + pathComponent);
    }
    return stringBuilder.toString();
  }
}

MyPathTests.java

import static org.hamcrest.Matchers.is;
import static org.hamcrest.collection.IsCollectionWithSize.hasSize;
import static org.hamcrest.collection.IsEmptyCollection.empty;
import static org.hamcrest.collection.IsIterableContainingInOrder.contains;
import static org.hamcrest.core.IsEqual.equalTo;
import static org.hamcrest.core.IsNot.not;
import static org.hamcrest.core.IsNull.notNullValue;
import static org.junit.Assert.assertThat;
import org.junit.Test;
import org.junit.experimental.runners.Enclosed;
import org.junit.runner.RunWith;
import com.google.common.base.Joiner;
@RunWith(Enclosed.class)
public class MyPathTests {
  public static class GetComponents {
    @Test
    public void componentsCorrespondToFactoryArguments() {
      String[] components = { "Test1", "Test2", "Test3" };
      MyPath myPath = MyPath.ofComponents(components);
      assertThat(myPath.getComponents(), contains(components));
    }
  }
  public static class OfComponents {
    @Test
    public void acceptsArrayOfComponents() {
      MyPath.ofComponents("Test1", "Test2", "Test3");
    }
    @Test
    public void acceptsSingleComponent() {
      MyPath.ofComponents("Test1");
    }
    @Test(expected = IllegalArgumentException.class)
    public void emptyStringVarArgsThrows() {
      MyPath.ofComponents(new String[] { });
    }
    @Test(expected = NullPointerException.class)
    public void nullStringVarArgsThrows() {
      MyPath.ofComponents((String[]) null);
    }
    @Test(expected = IllegalArgumentException.class)
    public void rejectsInterspersedEmptyComponents() {
      MyPath.ofComponents("Test1", "", "Test2");
    }
    @Test(expected = IllegalArgumentException.class)
    public void rejectsSingleEmptyComponent() {
      MyPath.ofComponents("");
    }
    @Test
    public void returnsNotNullValue() {
      assertThat(MyPath.ofComponents("Test"), is(notNullValue()));
    }
  }
  public static class Root {
    @Test
    public void hasComponents() {
      assertThat(MyPath.ROOT.getComponents(), is(not(empty())));
    }
    @Test
    public void hasExactlyOneComponent() {
      assertThat(MyPath.ROOT.getComponents(), hasSize(1));
    }
    @Test
    public void hasExactlyOneInboxComponent() {
      assertThat(MyPath.ROOT.getComponents(), contains("ROOT"));
    }
    @Test
    public void isNotNull() {
      assertThat(MyPath.ROOT, is(notNullValue()));
    }
    @Test
    public void toStringIsSlashSeparatedAbsolutePathToInbox() {
      assertThat(MyPath.ROOT.toString(), is(equalTo("/ROOT")));
    }
  }
  public static class ToString {
    @Test
    public void toStringIsSlashSeparatedPathOfComponents() {
      String[] components = { "Test1", "Test2", "Test3" };
      String expectedPath =
          MyPath.SEPARATOR + Joiner.on(MyPath.SEPARATOR).join(components);
      assertThat(MyPath.ofComponents(components).toString(),
          is(equalTo(expectedPath)));
    }
  }
  @Test
  public void testPathCreationFromComponents() {
    String[] pathComponentArguments = new String[] { "One", "Two", "Three" };
    MyPath myPath = MyPath.ofComponents(pathComponentArguments);
    assertThat(myPath.getComponents(), contains(pathComponentArguments));
  }
}

Вопрос, явно сформулированный

  • Есть ли список методов, используемых для построения unit test? Что-то гораздо более продвинутое, чем приведенный выше упрощенный список (например, проверка нулей, проверка границ, проверка ожидаемых исключений и т.д.), Возможно, доступная в книге для покупки или URL-адресе для посещения?

  • Как только у меня есть метод, который принимает определенный тип параметров, могу ли я получить плагин Eclipse чтобы создать для меня тесты для моих тестов? Возможно, используя Java Annotation, чтобы указать метаданные о методе и иметь средство для материализации соответствующих проверок для меня? (например, @MustBeLowerCase, @ShouldBeOfSize (n = 3),...)

Мне кажется утомительным и роботоподобным, что нужно помнить обо всех этих "трюках QA" и/или применять их, я нахожу его склонным к ошибкам копировать и вставлять, и я нахожу его не самодокументирующим, когда я кодирую вещи как я делаю выше. По общему признанию, Hamcrest libraries идет в общем направлении специализированных типов тестов (например, на String с использованием RegEx, на объектах File и т.д.), но, очевидно, не автогенерируют любые тестовые заглушки и не задумывайтесь над кодом и его свойствами и подготовьте для меня проводку.

Помогите мне сделать это лучше, пожалуйста.

PS

Не говорите мне, что я просто представляю код, который является глупым оберткой вокруг концепции создания пути из списка шагов пути, предоставляемых статическим методом factory, пожалуйста, это полностью выдуманный пример, но он показывает "несколько" случаев проверки аргументов... Если бы я включил гораздо более длинный пример, кто бы действительно прочитал этот пост?

4b9b3361

Ответ 1

  • Рассмотрим ExpectedException вместо @Test(expected.... Это связано с тем, что если вы ожидаете, что NullPointerException, и ваш тест выдает это исключение в вашей настройке (перед вызовом тестируемого метода) ваш тест пройдет. С помощью ExpectedException вы помещаете ожидание непосредственно перед вызовом тестируемого метода, поэтому нет никаких шансов на это. Кроме того, ExpectedException позволяет протестировать сообщение об исключении, которое полезно, если у вас есть два разных IllegalArgumentExceptions, которые могут быть выбраны, и вам нужно проверить правильность.

  • Рассмотрите возможность изолировать тестируемый метод от установки и проверить, это облегчит проверку и обслуживание. Это особенно актуально, когда методы в тестируемом классе вызывается как часть настройки, которая может запутать тот, который является тестируемым методом. Я использую следующий формат:

    public void test() {
       //setup
       ...
    
       // test (usually only one line of code in this block)
       ...
    
       //verify
       ...
    }
    
  • Книги для просмотра: Очистить код, JUnit In Действие, Разработка, основанная на тестах, по примеру

    У Clean Code есть отличный раздел при тестировании

  • В большинстве примеров, которые я видел (в том числе, что автогенерация Eclipse), есть метод, который тестируется в заголовке теста. Это облегчает обзор и обслуживание. Например: testOfComponents_nullCase. Ваш пример - первый, который я видел, который использует Enclosed для группировки методов методом test, что очень приятно. Однако он добавляет некоторые накладные расходы, поскольку @Before и @After не разделяются между закрытыми тестовыми классами.

  • Я не начал использовать его, но у Guava есть тестовая библиотека: guava-testlib. У меня не было шанса поиграть с ним, но, похоже, у него классный материал. Например: NullPointerTest - это цитата:

  • Утилита проверки, которая проверяет, что ваши методы бросают {@link * NullPointerException} или {@link UnsupportedOperationException}, когда любой из их параметров имеет значение NULL. Чтобы использовать его, вы должны сначала предоставить допустимые значения по умолчанию * для типов параметров, используемых классом.

Обзор: Я понимаю, что тест выше был всего лишь примером, но поскольку конструктивный обзор может быть полезен, вы здесь.

  • При тестировании getComponents также проверьте пустой список. Кроме того, используйте IsIterableContainingInOrder.

  • При тестировании ofComponents кажется, что имеет смысл вызвать getComponents или toString, чтобы проверить, правильно ли он обрабатывал различные случаи без ошибок. Там должен быть тест, где аргумент не передается ofComponents. Я вижу, что это делается с помощью ofComponents( new String[]{}), но почему бы просто не сделать ofComponents()? Нужна проверка, где null - одно из значений: ofComponents("blah", null, "blah2"), так как это вызовет NPE.

  • При тестировании ROOT, как уже указывалось ранее, я предлагаю позвонить ROOT.getComponents один раз и выполнить на нем все три проверки. Кроме того, ItIterableContainingInOrder делает все три не пустых, размер и содержит. is в тестах является экстралогичным (хотя он лингвистичен), и я чувствую, что не стоит иметь (ИМХО).

  • При тестировании toString, я считаю, что очень полезно изолировать тестируемый метод. Я бы написал toStringIsSlashSeparatedPathOfComponents следующим образом. Обратите внимание, что я не использую константу из тестируемого класса. Это связано с тем, что IMHO, ЛЮБОЙ функциональный переход к тестируемому классу должен привести к сбою теста.

    @Test     
    public void toStringIsSlashSeparatedPathOfComponents() {       
       //setup 
       String[] components = { "Test1", "Test2", "Test3" };       
       String expectedPath =  "/" + Joiner.on("/").join(components);   
       MyPath path = MyPath.ofComponents(components)
    
       // test
       String value = path.toStrign();
    
       // verify
       assertThat(value, equalTo(expectedPath));   
    } 
    
  • Enclosed не будет запускать никаких unit test, которые не входят во внутренний класс. Поэтому testPathCreationFromComponents не будет запущен.

Наконец, используйте Test Driven Development. Это гарантирует, что ваши тесты пройдут по правильной причине и сработают, как ожидалось.

Ответ 2

Я вижу, что вы приложили много усилий, чтобы действительно проверить свои классы. Хорошо!:)

Мои комментарии/вопросы:

  • а как насчет насмешек? вы не упоминаете какой-либо инструмент для этого
  • Мне кажется, что вы много заботитесь о подробных деталях (я не говорю, что они не важны!), не обращая внимания на бизнес-цель тестируемого класса. Я предполагаю, что это происходит из-за того, что вы сначала кодируете код (не так ли?). Я бы предложил больше подхода TDD/BDD и сосредоточиться на бизнес-обязанностях тестируемого класса.
  • Не уверен, что это дает вам: "Тестирование методов сгруппировано в статические вложенные классы"?
  • относительно автоматической генерации тестовых заглушек и т.д. Проще говоря: не надо. В результате вы будете тестировать реализацию вместо поведения.

Ответ 3

Хорошо, вот мое мнение по вашим вопросам:

Есть ли список методов, используемых для построения unit test?

Короткий ответ, нет. Ваша проблема заключается в том, что для генерации теста для метода вам необходимо проанализировать, что он делает, и поместить тест для каждого возможного значения в каждом месте. Существуют/были генераторы тестов, но IIRC, они не генерировали поддерживаемый код (см. Ресурсы для разработки, основанной на тестах).

У вас уже есть довольно хороший список вещей для проверки, к которым я бы добавил:

  • Убедитесь, что все пути через ваши методы закрыты.
  • Убедитесь, что все важные функции охвачены более чем одним тестом, для этого я использую параметр Parameterized.

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

Еще одна полезная вещь - это сократить шаблон, связанный с тестами, поэтому я могу легче читать тесты. Чем проще добавлять тесты, тем лучше. Я считаю, что Parameterized очень хорош для этого. Для меня важна читаемость тестов.

Итак, взяв ваш пример выше, если мы отбросим требование "проверить только одно в методе", мы получим

public static class Root {
  @Test
  public void testROOT() {
    assertThat("hasComponents", MyPath.ROOT.getComponents(), is(not(empty())));
    assertThat("hasExactlyOneComponent", MyPath.ROOT.getComponents(), hasSize(1));
    assertThat("hasExactlyOneInboxComponent", MyPath.ROOT.getComponents(), contains("ROOT"));
    assertThat("isNotNull", MyPath.ROOT, is(notNullValue()));
    assertThat("toStringIsSlashSeparatedAbsolutePathToInbox", MyPath.ROOT.toString(), is(equalTo("/ROOT")));
  }
}

Я сделал две вещи: я добавил описание в assert, и я объединил все тесты в один. Теперь мы можем прочитать тест и убедиться, что у нас есть двойные тесты. Вероятно, нам не нужно тестировать is(not(empty()) && is(notNullValue()) и т.д. Это нарушает правило assert для каждого метода, но я считаю это обоснованным, потому что вы удалили множество шаблонов без сокращения покрытия.

Можно ли выполнять проверки автоматически?

Да. Но я бы не использовал аннотации для этого. Скажем, у нас есть такой метод, как:

public boolean validate(Foobar foobar) {
  return !foobar.getBar().length > 40;
} 

Итак, у меня есть тестовый метод, который говорит что-то вроде:

private Foobar getFoobar(int length) {
  Foobar foobar = new Foobar();
  foobar.setBar(StringUtils.rightPad("", length, "x")); // make string of length characters
  return foobar;
}

@Test
public void testFoobar() {
  assertEquals(true, getFoobar(39));
  assertEquals(true, getFoobar(40));
  assertEquals(false, getFoobar(41));
}

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

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

@RunWith (Parameterized.class) public static class OfComponents {   @Parameters   public static Данные коллекции() {         return Arrays.asList(новый объект [] [] {         {new String [] { "Test1", "Test2", "Test3" }, null},         {new String [] { "Test1" }, null},         {null, NullPointerException.class},         {new String [] { "Test1", "", "Test2" }, IllegalArgumentException},       });   }

private String[] components;

@Rule
public TestRule expectedExceptionRule = ExpectedException.none();

public OfComponents(String[] components, Exception expectedException) {
   this.components = components;
   if (expectedException != null) {
     expectedExceptionRule.expect(expectedException);
   }
}

@Test
public void test() {
  MyPath.ofComponents(components);
}

Обратите внимание, что вышеуказанное не проверено и, вероятно, не компилируется. Из вышесказанного вы можете анализировать данные как входные данные и добавлять (или, по крайней мере, думать о добавлении) все комбинации всего. Например, у вас нет теста для { "Test1", null, "Test2" }...

Ответ 4

Хорошо, я опубликую 2 разностных ответа.

  • Как сказал Джеймс Коплиен, unit test бесполезен. Я не согласен с ним в этом вопросе, но, возможно, вам будет полезно рассмотреть unit test меньше, чем искать автоматическое решение.

  • Предположим использовать теории с DataPoints. Я думаю, что это значительно уменьшит вашу проблему. Кроме того, использование mock может помочь вам.