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


Интересна Java? Кликай по ссылке и изучай!
Если тебе полезно что-то из того, чем я делюсь в своем блоге - можешь поделиться своими деньгами со мной.
с пожеланием
столько времени читатели провели на блоге - 
сейчас онлайн - 

среда, 23 июня 2010 г.

JUnit хитрости: Tихие asserts

Не так давно я рассказывал как я проверяю исключительные ситуации в своем TDD. Сегодня поделюсь еще одним приемом, который я назвал "тихими ассертами" (quiet asserts). Бывает приходится писать (или выделять из теста) сложный пользовательский assert. Естественно такой assert состоит из нескольких примитивных. Но вот беда, когда слетает первый assert из этого пользовательского набора, то мы теряем возможность увидеть картину в целом. На помощь придут "тихие ассерты". Читаем дальше...

Допустим есть некий объект Info, полями которого есть: qwe и asd. Вот он
public class Info {
    public int qwe;
    public int asd;
}
Забудем на время про инкапсуляцию, я хочу сэкономить мне и тебе время.

Допустим есть некоторое число тестов и мы заметили в них общую логику:
public vois testCase1() {
    Info info = getService().getInfo1();
    assertEquals("Qwe с ошибкой", 1, info.qwe);
    assertEquals("Asd с ошибкой", 10, info.asd);
}

public vois testCase2() {
    Info info = getService().getInfo2();
    assertEquals("Qwe с ошибкой", 2, info.qwe);
    assertEquals("Asd с ошибкой", 20, info.asd);
}
Выделим ее в новый пользовательский ассерт.
public static void assertInfoValue(int expectedQwe, int expectedAsd, Info actualInfo) {
    assertEquals("Qwe с ошибкой", expectedQwe, actualInfo.qwe);
    assertEquals("Asd с ошибкой", expectedAsd, actualInfo.asd);
}
Тогда наши тесты упростятся
public vois testCase1() {
    assertInfoValue(1, 10, getService().getInfo1());
}

public vois testCase2() {
    assertInfoValue(2, 20, getService().getInfo1());
}
Уверен nы делал так неоднократно. До недавнего времени я ограничивался этим подходом. Были глюки, но меня все устраивало. Вот, к примеру, один кейс из прошлого. Допустим как-то случилось так, что тест слетел на первом ассерте из пользовательского assertInfoValue. Я увидел в консоли Junit "Qwe с ошибкой". В чем проблема я не знаю. Пускай кто-то обновил базу и теперь много тестов послетали. После обдумывания, я понимаю в чем дело и добавляю недостающую запись в базу. Снова запускаю тест и вижу уже "Asd с ошибкой". "Ааааа!" - думаю я - "я забыл еще одну запись добавить". Снова добавляю какую-то запиьс и все - тест зелененький! Знакомо?

Если бы сразу можно было увидеть информацию про оба несоответствия - я бы внес в базу все необходимые изменения, чем сэкономил бы драгоценное время. Раньше меня это не парило, пока не пришел проект со сложными объектами, пользовательские asserts которых состояли из множества примитивных проверок, причем часто в циклах коллекций. Вот тут то и пришло просветление в виде "тихих ассертов". Кстати, на идею меня натолкнула фраза "тихо не лезь, я еще не закончил", рядом сидящих и в чем-то разбирающихся напарников. Спасибо, ребята.

Итак продолжим. Assert наш мы перепишем.
public static void assertInfoValue(int expectedQwe, int expectedAsd, Info actualInfo) {
    AssertionError error1 = assertQuietEquals("Qwe с ошибкой", expectedQwe, actualInfo.qwe);
    AssertionError error2 = assertQuietEquals("Asd с ошибкой", expectedAsd, actualInfo.asd);
}
А так как у нас нет еще нужного метода, мы его добавим (надеюсь средствами IDE пользуетесь?)
public static AssetionError assertQuietEquals(String message, Object expected, Onject actual) {
    try {
        assertEquals(message, expected, actual);
    } catch (AssertionError error) {
        return error;
    }
}
Все компилится - супер, но проблема такого теста в том, что он всегда будет зеленый. Это мы и поправим, добавив одну строчку кода в конец нашего теста
public static void assertInfoValue(int expectedQwe, int expectedAsd, Info actualInfo) {
    AssertionError error1 = ...
    AssertionError error2 = ...
    throwAllAssertionErrors(error1, error2);
}
Метода throwAllAssertionErrors нет, но мы его создадим.
public static void throwAllAssertionErrors(AssertionError... errors) {
    Collection<string> messages = new ArrayList<string>(errors.length);
    for (AssertionError error : errors){
        if (error != null){
            messages.add(error.getMessage());
        }
    }
    if (messages.size() == 0){
        return;
    }

    String message = mergeStringCollection(messages, "\n");
    throw new AssertionError(message);
}
    
private static String mergeStringCollection(Collection messages, String separator) {
    Iterator iterator = messages.iterator();
    StringBuilder sb = new StringBuilder();
    for ( ;; ) {
        Object e = iterator.next();
        sb.append(e);
        if (!iterator.hasNext()){
            return sb.toString();
        }
        sb.append(separator);
    }
}
Я не переписывал код наново (как все примеры в статье), а влепил кусок готового рабочего кода. Если его надо порефакторить - прошу, предлагайте конечный вариант. В свое время я его написал на коленке и мне этого достаточно.

D результате работы такого пользовательского assert(если оба примитивных слетело) мы увидим сообщение об ошибке:
Qwe с ошибкой. expected: 1, but was: 150
Asd с ошибкой. expected: 10, but was: 1500
...
А это то, что я хотел.

Есть недостаток - если ты забудешь в конце теста вызвать throwAllAssertionErrors, то тест пройдет что бы ни случилось. Но я надеюсь что твой тест хоть раз был красным ;) Можно так же пофантазировать на тему вызова throwAllAssertionErrors в tearDown, но пускай это будет твоим домашним заданием.

Удачи!

В следующем выпуске "JUnit хитрости: Объединяем stack traces" мы немного улучшим этот подход.
Вот, кстати еще один способ решить эту проблему JUnit хитрости: Ассертим все и сразу

3 комментария:

  1. Selenium IDE генерирует код тестов именно таким способом, я был не на шутку удивлен когда заранее провальный тест доработал до конца, и только потом выплюнул кучу провалов.

    ОтветитьУдалить