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


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

четверг, 17 июня 2010 г.

JUnit хитрости: Ловля исключений

Если ты разрабатываешь в стиле Test Driven Development, то вероятно как и я хочешь вытянуть их jUnit по максимуму. Возможно на те кейсы, что я опишу сейчас, есть спец фреймворки (если тебе об таковых известно - скинь линк, плз) - я об ниж пока не знаю.

Итак первый кейс - обработка исключительный ситуаций. Читаем дальше...

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

Самый простой способ - проверить что эксцепшен в принципе возник. Сделаем это:
public void testCheckIfLessThen0() {
    Integer someParam = -1; // бага тут
    try {
        object.someMethod(someParam); // тестим это
    } catch (Exception e) {
    }
}
Скажу сразу этот тест будет зеленым даже если не возникло никакой исключительной ситуации. Это первая и часто распространенная ошибка. Чтобы ее исправить добавим одну строчку.
public void testCheckIfLessThen0() {
    Integer someParam = -1; // бага тут
    try {
        object.someMethod(someParam); // тестим это
        fail("Exception expected");
    } catch (Exception e) {
    }
}
Так лучше. Но когда мы напишем второй тестовый случай.
public void testCheckIfNull() {
    Integer someParam = null; // бага тут
    try {
        object.someMethod(someParam); // тестим это
        fail("Exception expected");
    } catch (Exception e) {
    }
}
То поймем (ибо тест новый сразу будет зеленый), что хорошо бы нам как-то конкретизировать какая именно ошибка возникла. Кстати, по этой причине мне не нравится подход Junit 4 к ловле exception с помощью аргумента expected аннотации @Test - не доработали ребята.

В общем мы сделаем так
public void testCheckIfNull() {
    Integer someParam = null; // бага тут
    try {
        object.someMethod(someParam); // тестим это
        fail("Exception expected");
    } catch (Exception exception) {
        assertEquals("Мы ожидали другое.", "Аргумент не может быть null", exception.getMessage());
    }
}
И то же для другого теста.
public void testCheckIfLessThen0() {
    Integer someParam = -1; // бага тут
    try {
        object.someMethod(someParam); // тестим это
        fail("Exception expected");
    } catch (Exception exception) {
        assertEquals("Мы ожидали другое.", "Аргумент не может быть меньше нуля", exception.getMessage());
    }
}
Хорошо то как! Но это довольно примитивный случай, и скорее всего сообщение об ошибке будет находиться где-то очень глубоко. К примеру все могло быть так
public void testCheckIfLessThen0() {
    Integer someParam = -1; // бага тут
    try {
        object.someMethod(someParam); // тестим это
        fail("Exception expected");
    } catch (Exception exception) {
        EJBException ejbException = (EJBException)exception;
        Bla1xception blaException = (BlaException)ejbException.getParent();
        SqlException sqlException = blaException.getSqlException();
        assertEquals("Мы ожидали другое.", "База говорит, что аргумент не может быть меньше нуля", sqlException.getMessage());
    }
}
Но тут есть проблема - мы использовали множество class cast при этом не проверяя предварительно типы, и если возникнет какая-то другая исключительная ситуация, то мы увидим следствие этого провтыка а не причину слетевшего теста.

Я, а может и ты тоже - пошел в свое время очевидным путем - перед каждым кейсом делал дополнительно assertEquals по классу. Вот так
public void testCheckIfLessThen0() {
    Integer someParam = -1; // бага тут
    try {
        object.someMethod(someParam); // тестим это
        fail("Exception expected");
    } catch (Exception exception) {
        assertEquals("Чето не тот exception пришел", EJBException.class, exception.getClass());
        EJBException ejbException = (EJBException)exception;
        assertEquals("Чето не тот exception пришел", Bla1xception.class, ejbException.getClass());
        Bla1xception blaException = (BlaException)ejbException.getParent();
        assertEquals("Чето не тот exception пришел", SqlException.class, blaException.getClass());
        SqlException sqlException = blaException.getSqlException();
        assertEquals("Мы ожидали другое.", "База говорит, что аргумент не может быть меньше нуля", sqlException.getMessage());
    }
}
Ну и ясное дело, раз уж это будет повторяться не один раз - используем мною любимый ExtractMethod.
public void testCheckIfLessThen0() {
    Integer someParam = -1; // бага тут
    try {
        object.someMethod(someParam); // тестим это
        fail("Exception expected");
    } catch (Exception exception) {
        assertSQLException(exception, "База говорит,что аргумент не может быть меньше нуля")
    }
}

public static assertSQLException(Exception actualException, String expectedMessage) {      
    assertEquals("Чето не тот exception пришел", EJBException.class, actualException.getClass());
    EJBException ejbException = (EJBException)actualException;
    assertEquals("Чето не тот exception пришел", Bla1xception.class, ejbException.getClass());
    Bla1xception blaException = (BlaException)ejbException.getParent();
    assertEquals("Чето не тот exception пришел", SqlException.class, blaException.getClass());
    SqlException sqlException = blaException.getSqlException();
    assertEquals("Мы ожидали другое.", expectedMessage, sqlException.getMessage());
}
Долго я пользовался этим подходом, пока не заметил одну закономерность - иногда раз, когда слетал подобный assert мне приходилось лезть в дебаггер и изучать что на самом деле слетело. Я делал большую ошибку, согласен - я отлаживал тест. Но это было не так часто и для разнообразия я себе это позволял. Задумался об этом тогда, когда определенная задача заставляла сделать подобный дебаг несколько раз подряд причем в сложно запутанном коде (где нельзя было сказать сразу какая его часть вызвала исключительную ситуацию).

И тут пришла идея. Если мне нужна информация про exception, отличный от ожидаемого в полной мере, то почему бы мне не прокидывать его? Так и сделал.
public static assertSQLException(Exception actualException, String expectedMessage) {      
    try {
        assertEquals("Чето не тот exception пришел", EJBException.class, actualException.getClass());
        EJBException ejbException = (EJBException)actualException;
        assertEquals("Чето не тот exception пришел", Bla1xception.class, ejbException.getClass());
        Bla1xception blaException = (BlaException)ejbException.getParent();
        assertEquals("Чето не тот exception пришел", SqlException.class, blaException.getClass());
        SqlException sqlException = blaException.getSqlException();
        assertEquals("Мы ожидали другое.", expectedMessage, sqlException.getMessage());
    } catch (AssertionError e) {
         throw actualException;
    }
}
Ну вот как бы и все. Чуть позже расскажу об тихих ассертах...

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

  1. "Кстати, по этой причине мне не нравится подход Junit 4 к ловле exception с помощью аргумента expected аннотации @Test - не доработали ребята. "

    Вроде в последних JUnit (~4.8) подход к ловле иксепшнов гораздо более продвинутый. Попробуй!

    ОтветитьУдалить
  2. Попробуйте вот такой подход http://weblogs.java.net/blog/johnsmart/archive/2009/09/27/testing-exceptions-junit-47

    ОтветитьУдалить
  3. Да да, уже опробовал эту фичу. Спасибо!

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