Javenue logo

Javenue

Программирование на Java

Информационные технологии

Тестирование Java кода с помощью JUnit - Туториал

English version of this article you can find here.

Тестирование далеко не всегда бывает веселым и интересным. Этот процесс обычно достаточно продолжителен и порой полон монотонной работы. Кажется, еще совсем недавно программисты пользовались стандартным выводом или же дебаггером для тестирования java классов.

В этой статье я опишу библиотеку JUnit 4, которая во многом упрощает и автоматизирует процесс написания тестов.

Для демонстрации основных возможностей JUnit Framework, напишем примитивный класс на языке Java и будем над ним издеваться. Этот класс будет иметь два метода - нахождение факториала неотрицательного числа и суммы двух чисел. Кроме того, в экземпляре класса будет находится счетчик вызовов методов.

public class MathFunc {
    int calls;

    public int getCalls() {
        return calls;
    }

    public long factorial(int number) {
        calls++;

        if (number < 0)
            throw new IllegalArgumentException();

        long result = 1;
        if (number > 1) {
            for (int i = 1; i <= number; i++)
                result = result * i;
        }

        return result;
    }

    public long plus(int num1, int num2) {
        calls++;
        return num1 + num2;
    }
}

Теперь напишем Unit тесты. Для этого создадим класс с некоторым количеством тестовых методов. Естественно, класс может содержать и обычные вспомогательные методы. Чтобы runner тестов мог определить, кто есть кто, тестовые методы необходимо помечать аннтоацией @Test.

У аннотации могут быть проставлены такие параметры:

  • expected - указываем какое исключение будет сгенерировано методом (см. пример ниже);
  • timeout - через какое время в милисекундах прекратить выполнение теста и засчитать его как неуспешный.

Если вы хотите указать, что определенный тест необхдимо пропустить, то пометьте его аннотацией @Ignore. Хотя можно просто удалить аннотацию @Test.

Бывает такое, что для выполнения каждого тестового сценария вам необходим некоторый контекст, например, заранее созданные экземпляры классов. А после выполнения нужно освободить зарезервированные ресурсы. В этом случае вам понадобятся аннтоации @Before и @After. Метод, помеченный @Before будет выполняться перед каждым тестовым случаем, а метод, помеченный @After - после каждого тестового случая.

Если же инициализацию и освобождение ресурсов нужно сделать всего один раз - соответственно до и после всех тестов - то используйте пару аннотаций @BeforeClass и @AfterClass.

А вот и сам тестовый класс с несколькими тестовыми сценариями:

public class MathFuncTest {
    private MathFunc math;

    @Before
    public void init() { math = new MathFunc(); }
    @After
    public void tearDown() { math = null; }

    @Test
    public void calls() {
        assertEquals(0, math.getCalls());

        math.factorial(1);
        assertEquals(1, math.getCalls());

        math.factorial(1);
        assertEquals(2, math.getCalls());
    }

    @Test
    public void factorial() {
        assertTrue(math.factorial(0) == 1);
        assertTrue(math.factorial(1) == 1);
        assertTrue(math.factorial(5) == 120);
    }

    @Test(expected = IllegalArgumentException.class)
    public void factorialNegative() {
        math.factorial(-1);
    }

    @Ignore
    @Test
    public void todo() {
        assertTrue(math.plus(1, 1) == 3);
    }
}

Метод calls тестирует правильность счетчика вызовов. Метод factorial проверяет правильность вычисления факториала для некоторых стандартных значений. Метод factorialNegative проверяет, что для отрицательных значений факотриала будет брошен IllegalArgumentException. Метод todo будет проигнорирован. Попробуйте убрать аннотацию @Ignore, когда будете экспериментировать с кодом.

Метод assertTrue проверяет, является ли результат выражения верным. Некоторые другие методы, которые могут пригодиться:

  • assertEquals - ожидаемый результат и полученный результат совпадают;
  • assertNull - результатом выражения есть null;
  • assertNotNull - результат выражения отличен от null;
  • assertSame - ожидаемый и полученный объекты это один и тот же объект.
  • fail - метод генерирует исключение AssertionError - добавляем туда, куда не должен дойти ход выполнения программы.

В нашем современном мире IDE умеют находить и просто запускать тесты в проекте. Но что делать, если вы хотите запустить их вручную с помощью программного кода. Для этого можно воспользоваться Runner'ом. Бывают текстовый - junit.textui.TestRunner, графические версии - junit.swingui.TestRunner, junit.awtui.TestRunner.

Но чуть более современный метод - это использование класса JUnitCore. Добавьте следующий метод main в класс MathFuncTest:

    public static void main(String[] args) throws Exception {
        JUnitCore runner = new JUnitCore();
        Result result = runner.run(MathFuncTest.class);
        System.out.println("run tests: " + result.getRunCount());
        System.out.println("failed tests: " + result.getFailureCount());
        System.out.println("ignored tests: " + result.getIgnoreCount());
        System.out.println("success: " + result.wasSuccessful());
    }

И результат выполнения:

    run tests: 3
    failed tests: 0
    ignored tests: 1
    success: true

В более ранних версиях JUnit для написания тестового класса нужно было создать наследника junit.framework.TestCase. Затем необходимо было определить конструктор, принимающий в качестве параметра String - название метода - и передать его родительскому классу. Каждый тестовый метод обязан был начинаться с префикса test. Для инициализации и освобождения ресурсов использовались методы setUp и tearDown. Короче ужас. Ну а сейчас все просто, да.

Вот и все на сегодня. Уверен, JUnit Framework во многом поможет вам. Комментарии и вопросы по поводу статьи приветствуются.


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

  Выйти

  * для публикации комментариев нужно  

GX3P:

Здравствуйте,

Разъясните, пожалуйста, ситуацию. В чем смысл JUnit? Например, строки

MathFunc math = new MathFunc(45);
assertTrue(math.plus(123) == 168);

не являются ли они надуманными, в плане проверки? Ведь при отладке метода мы все равно проверяем правильность его работы. Так в чем тогда смысл этих строк? Создается впечатление искусственности ситуации.

Действительно хочу понять. Заранее спасибо,

Taper:

To GX3P:

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

На этом простом примере автор показал принцип использования JUnit с использованием простых операций, которые можем проверить самостоятельно: 45 + 123 = 168.

Отладка: бывает разная, все зависит от приложения - есть случаи когда без ручной отладки не обойтись никак (для этого собирают команды тестировщиков), а есть случаи, когда простые вещи можно проверить еще на этапе сборки приложения - для этого и создаются JUnit-тесты. Ведь согласитесь, гораздо приятнее когда работу человека выполняет железяка, да еще и быстро, и к тому же на этапе компиляции.

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

Очень редко случается что класс пишется один раз и навсегда, SDK и тот обновляется периодически. Вот отсюда и появляется необходимость в автоматических тестах.