Книга 1 - Начальные сведения
Книга 2 - Более профессиональный подход |
Студенческий отдел кадров Тестирование с точки зрения разработчика
Существует целая область знаний посвященная тестам. Тесты типа "черного ящика", тесты типа
"белого ящика", стратегии тестирования и прочая.
Так какова стратегия написания тестов для нашего случая ?
Т.к. база может меняться, то мы должны иметь быстрый способ проверить - все ли методы, которые
работают непосредственно с базой, в порядке. Можно написать простые методы, которые что-то
модифицируют в базе и хотя бы не вызывают ошибки выполнения. Уже прогресс. Добавили поле и
запустили сразу тесты - получили список мест, где надо исправлять. Можно сделать глупый на
первый взгляд тест - записали в базу, считали и проверили с тем, что раньше записывали. Да что
там может случится? Оказывается может. Триггер разработчики базы ввели. А мы не знали. И получаем
не то, что рассчитывали. Теперь либо меняем тест, либо меняем код. Стадии тестированияЗа долгие годы существования понятия unit-тестирования были выработаны некоторые подходы, которые помогают несколько автоматизировать создание тестов. "Автоматизировать" в данном случае не означает, что за Вас какая-то программа напишет тесты. Она означает, что есть определенная стратегия написания, которая упрощает работы над созданием тестов. Некоторые тесты можно запустить и проверить программно, некоторые (особенно это касается UI) - крайне сложно. Для того же UI бывает удобно просто записать движения мышки, нажатие клавиатуры и потом эти действия "натравить" на Вашу систему. Ну и там отслеживать, что и как обрабатывает такое поведение. Каждый раз при запуске тест должен пройти через 4 стадии:
Инициализация
Запуск
Проверка
Освобождение ресурсов Но реализация этих этапов - задача достаточно творческая. Надо копить опыт, придумывать, пробовать. И когда ты начинаешь понимать и принимать идею unit-тестов - они становятся очень интересной областью. Так что получайте удовольствие :) А теперь мы расмотрим один из самых известных продуктов для создания Unit-тестов - JUnit JUnit - один из самых распространенных framework для тестированияПо большому счету JUnit прямо-таки издевательски простой пакет. Вся его задача состоит в том, чтобы удобно запустить некий класс, который будет состоять из функций предназначенных для тестирования Вашего приложения. В нем все предназначено только для одной цели - удобно писать и запускать тесты, которые Вы придумали сами. В общем-то и все. Если этот пакет освоить хорошо, то он становиться так же необохдим и удобен как тот же FAR или TotalCommander.
Мы рассмотрим JUnit на примере сравнительно новой версии 4.4 (со временем она тоже будет устаревшей).
Загрузить ее можно с сайта - http://www.junit.org/
Наверху справа есть пункт "Download JUnit". Дальше Вас кинут на sourceforge.net
Первый простой тест public class Calc { public int getSum(int x, int y) { return x+y; } } Теперь мы должны написать класс, который тестирует наше "сложное" приложение. В отличии от версии 3.8, где тестирующий класс должен был наследоваться от класса TestCase и все методы для тестирования должны были начинаться с "test" (Например: testSum, testCalculation и т.д.), теперь этого всего не надо. Все необходимое нам заменяет аннотация - об этом Вы можете прочесть в статье Java 5.0". Наш класс выглядит очень просто import org.junit.Test; // Еще одна "вкусность" Java 5 - можно импортировать static-методы import static org.junit.Assert.assertEquals; public class TestCalc { // @Test - это аннотация, которая обозначает, // что метод должен быть вызван для тестирования чего-нибудь @Test public void getSumTest() { Calc c = new Calc(); // Этот метод вызовет исключение, если результат нашего калькулятора будет отличен от 50 assertEquals(c.getSum(20, 30), 50); } }
Положите в каталог с нашими классами файл junit-4.4.jar (Который Вы должны скачать). Для сборки
нашего приложения запускаем команду:
Теперь запускаем Ключик (-ea) разрешает обработку assert - в общем-то особо нам это не важно, но как-то привычнее. И ... на экране много не видно :)
JUnit version 4.4
.
Time: 0,031
OK (1 test)
Это все. Информации в общем-то немного и запускать тоже пока не очень удобно. Количество
точек после строки JUnit version 4.4 указывает, сколько тестов мы прошли. Как Вы возможно
догадались все тесты исполняет специальный "запускатель" JUnitCore. Ему мы передаем класс,
который будем тестировать. public class Calc { public int getSum(int x, int y) { return x+y; } public int getSubtraction(int x, int y) { return x-y-1; } } Как видите наш второй метод содержит ошибку - разность в общем-то не так вычисляется. Класс TestCalc мы тоже должны переписать. Точнее дополнить еще одним методом для тестирования вычитания. Но тестирует он "верную" версию - т.е. вычитает по правилам арифметики. import org.junit.Test; import static org.junit.Assert.assertEquals; public class TestCalc { // @Test - это аннотация, которая обозначает, что метод должен быть вызван для тестирования чего-нибудь @Test public void getSumTest() { Calc c = new Calc(); // Этот метод вызовет исключение, если результат нашего калькулятора будет отличен от 50 assertEquals(c.getSum(20, 30), 50); } @Test public void getSubtractionTest() { Calc c = new Calc(); // Этот метод вызовет исключение, если результат нашего калькулятора будет отличен от -10 assertEquals(-10, c.getSubtraction(20, 30)); } }
Теперь перекомпилируем наши классы
Опять запускаем командой И видим:
JUnit version 4.4
..E
Time: 0,047
There was 1 failure:
1) getSubtractionTest(TestCalc)
java.lang.AssertionError: expected:<-10> but was:<-11>
at org.junit.Assert.fail(Assert.java:74)
at org.junit.Assert.failNotEquals(Assert.java:448)
at org.junit.Assert.assertEquals(Assert.java:102)
at org.junit.Assert.assertEquals(Assert.java:323)
at org.junit.Assert.assertEquals(Assert.java:319)
at TestCalc.getSubtractionTest(TestCalc.java:26)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:585)
at org.junit.internal.runners.TestMethod.invoke(TestMethod.java:59)
at org.junit.internal.runners.MethodRoadie.runTestMethod(MethodRoadie.java:98)
at org.junit.internal.runners.MethodRoadie$2.run(MethodRoadie.java:79)
at org.junit.internal.runners.MethodRoadie.runBeforesThenTestThenAfters(MethodRoadie.java:87)
at org.junit.internal.runners.MethodRoadie.runTest(MethodRoadie.java:77)
at org.junit.internal.runners.MethodRoadie.run(MethodRoadie.java:42)
at org.junit.internal.runners.JUnit4ClassRunner.invokeTestMethod(JUnit4ClassRunner.java:88)
at org.junit.internal.runners.JUnit4ClassRunner.runMethods(JUnit4ClassRunner.java:51)
at org.junit.internal.runners.JUnit4ClassRunner$1.run(JUnit4ClassRunner.java:44)
at org.junit.internal.runners.ClassRoadie.runUnprotected(ClassRoadie.java:27)
at org.junit.internal.runners.ClassRoadie.runProtected(ClassRoadie.java:37)
at org.junit.internal.runners.JUnit4ClassRunner.run(JUnit4ClassRunner.java:42)
at org.junit.internal.runners.CompositeRunner.runChildren(CompositeRunner.java:33)
at org.junit.internal.runners.CompositeRunner.run(CompositeRunner.java:28)
at org.junit.runner.JUnitCore.run(JUnitCore.java:130)
at org.junit.runner.JUnitCore.run(JUnitCore.java:109)
at org.junit.runner.JUnitCore.run(JUnitCore.java:100)
at org.junit.runner.JUnitCore.runMain(JUnitCore.java:81)
at org.junit.runner.JUnitCore.main(JUnitCore.java:44)
FAILURES!!!
Tests run: 2, Failures: 1
Тут уже информации гораздо больше. Видно, что не выполнен метод getSubstractionTest и
даже есть подсказка для чисел Но пока удобств все равно не так уж много. Мало информации, импользуется какой-то JUnitCore. Попробуем сделать наше приложение более информативным и удобным. Изменим наш тестирующий класс - оснастим его методом main. А то надоело вызывать JUnitCore. import org.junit.Test; import static org.junit.Assert.assertEquals; // Теперь мы не будем вызывать в командной строке JUnitCore import org.junit.runner.JUnitCore; public class TestCalc { @Test public void getSumTest() { Calc c = new Calc(); assertEquals(50, c.getSum(20, 30)); } @Test public void getSubtractionTest() { Calc c = new Calc(); assertEquals(-10, c.getSubtraction(20, 30)); } // Вот где у нас будет вызов нашего тестирующего класса public static void main(String[] args) { JUnitCore core = new JUnitCore(); core.run(TestCalc.class); } }
Ну теперь компилируем и запускаем import org.junit.Test; import static org.junit.Assert.assertEquals; import org.junit.runner.JUnitCore; import org.junit.runner.notification.RunListener; import org.junit.runner.notification.Failure; public class TestCalc { @Test public void getSumTest() { Calc c = new Calc(); assertEquals(50, c.getSum(20, 30)); } @Test public void getSubtractionTest() { Calc c = new Calc(); assertEquals(-10, c.getSubtraction(20, 30)); } public static void main(String[] args) { JUnitCore core = new JUnitCore(); // Вот подключение нашего собственного слушателя/листенера core.addListener(new CalcListener()); core.run(TestCalc.class); } } // А вот его реализация class CalcListener extends RunListener { // Пока мы сделали один метод - листенер будет реагировать только когда возникаю проблемы @Override public void testFailure(Failure fail) { System.out.println("Failed:" + fail.getDescription().getDisplayName() + " [" + fail.getMessage() + "]"); } } Если Вы откроете документацию и найдете описание RunListener, то сможете написать свои собственные методы для обработки начала тестирования метода, начала тестирования вообще и т.д. Советую прочитать внимательно. Для окончательного слова о листенерах приведем еще один пример кода. import org.junit.Test; import static org.junit.Assert.assertEquals; import org.junit.runner.JUnitCore; import org.junit.runner.Description; import org.junit.runner.notification.RunListener; import org.junit.runner.notification.Failure; public class TestCalc { // @Test - это аннотация, которая обозначает, что метод должен быть вызван для тестирования чего-нибудь @Test public void getSumTest() { Calc c = new Calc(); assertEquals(50, c.getSum(20, 30)); } @Test public void getSubtractionTest() { Calc c = new Calc(); assertEquals(-10, c.getSubtraction(20, 30)); } public static void main(String[] args) { JUnitCore core = new JUnitCore(); core.addListener(new CalcListener()); core.run(TestCalc.class); } } class CalcListener extends RunListener { @Override public void testStarted(Description desc) { System.out.println("Started:" + desc.getDisplayName()); } @Override public void testFinished(Description desc) { System.out.println("Finished:" + desc.getDisplayName()); } @Override public void testFailure(Failure fail) { System.out.println("Failed:" + fail.getDescription().getDisplayName() + " [" + fail.getMessage() + "]"); } }
Если Вам не требуется отслеживать имена методов, а просто получить известие о том, что
что-то пошло не так можно использовать аннотации
@Before, @After, @BeforeClass, @AfterClass.
Методы, которые снабжены такой аннотацией будут вызываться: в начале каждого метода, в конце
каждого метода, в начале тестирования, в конце тестирования. import org.junit.Test; import org.junit.Before; import org.junit.After; import org.junit.BeforeClass; import org.junit.AfterClass; import org.junit.Ignore; import static org.junit.Assert.assertEquals; import org.junit.runner.JUnitCore; public class TestCalc { // @Test - это аннотация, которая обозначает, что метод должен быть вызван для тестирования чего-нибудь @Test public void getSumTest() { Calc c = new Calc(); assertEquals(50, c.getSum(20, 30)); } @Test public void getSubtractionTest() { Calc c = new Calc(); assertEquals(-10, c.getSubtraction(20, 30)); } @BeforeClass public static void allTestsStarted() { System.out.println("All tests started"); } @AfterClass public static void allTestsFinished() { System.out.println("All tests finished"); } @Before public void testStarted() { System.out.println("Started"); } @After public void testFinished() { System.out.println("Finished"); } @Test // Обратите внимание аннотацию - она говорит, что тест будет проигнорирован. Если ее убрать, // то сообщение появиться @Ignore public void testIgnored() { System.out.println("Ignored test"); } public static void main(String[] args) { JUnitCore core = new JUnitCore(); core.run(TestCalc.class); } } Порядок прежде всегоБывают случаи, когда порядок вызовов методов важен дл ятестирования. В таком случае можно воспользоваться сортировкой. Давайте рассмотрим это на примере. import java.util.Comparator; import org.junit.Test; import static org.junit.Assert.assertEquals; import org.junit.runner.JUnitCore; import org.junit.runner.Description; import org.junit.runner.Request; import org.junit.runner.notification.RunListener; import org.junit.runner.notification.Failure; public class TestCalc { // Обратите внимание на название - оно теперь сделано таким для соблюдения порядка @Test public void get001SumTest() { Calc c = new Calc(); assertEquals(50, c.getSum(20, 30)); } // Обратите внимание на название - оно теперь сделано таким для соблюдения порядка @Test public void get002SubtractionTest() { Calc c = new Calc(); assertEquals(-10, c.getSubtraction(20, 30)); } // Метод возвращает компаратор, который позволяет отсортировать методы в алфавитном порядке private static Comparator forward() { return new Comparator() { public int compare(Object o1, Object o2) { Description d1 = (Description) o1; Description d2 = (Description) o2; return d1.getDisplayName().compareTo(d2.getDisplayName()); } }; } public static void main(String[] args) { JUnitCore core = new JUnitCore(); core.addListener(new CalcListener()); // Обратите внимание на этот вызов - ниже немного комментариев core.run(Request.aClass(TestCalc.class).sortWith(forward())); } } // Ну здесь по идее должно быть все понятно class CalcListener extends RunListener { @Override public void testStarted(Description desc) { System.out.println("Started:" + desc.getDisplayName()); } @Override public void testFinished(Description desc) { System.out.println("Finished:" + desc.getDisplayName()); } @Override public void testFailure(Failure fail) { System.out.println("Failed:" + fail.getDescription().getDisplayName() + " [" + fail.getMessage() + "]"); } } Итак, комментарии к вызову core.run(...). Здесь использован объект Request, который позволяет запускать не один, а сразу много классов для тестирования. В документации внимательно посмотрите на его методы и Вы сможете разобраться. Либо присылайте вопросы. А тестовые данные как использовать ?Вы можете определить набор тестовых данных, которые будут подаваться на вход тестирующего класса. Чтобы долго не растекаться по древу приведем пример. Комментарии смотрите после кода. import java.util.Arrays; import java.util.Collection; import org.junit.Test; import static org.junit.Assert.assertEquals; import org.junit.runner.JUnitCore; import org.junit.runner.Description; import org.junit.runner.RunWith; import org.junit.runner.notification.RunListener; import org.junit.runner.notification.Failure; import org.junit.runners.Parameterized; import static org.junit.runners.Parameterized.Parameters; @RunWith(Parameterized.class) public class TestCalc { // Обратите внимание на данные в скобках - первый два - числа, которые складываем/вычитаем // Вторые - это их сумма и разность @Parameters public static Collection data() { return Arrays.asList(new Object[][]{ {5, 3, 8, 2}, {15, 10, 25, 5}, {5, 10, 15, -5} }); } int x1, x2, sum, sub; public TestCalc(int x1, int x2, int sum, int sub) { this.x1 = x1; this.x2 = x2; this.sum = sum; this.sub = sub; } @Test public void getSumTest() { Calc c = new Calc(); assertEquals(sum, c.getSum(x1, x2)); } @Test public void getSubtractionTest() { Calc c = new Calc(); assertEquals(sub, c.getSubtraction(x1, x2)); } public static void main(String[] args) { JUnitCore core = new JUnitCore(); core.addListener(new CalcListener()); core.run(TestCalc.class); } } class CalcListener extends RunListener { @Override public void testStarted(Description desc) { System.out.println("Started:" + desc.getDisplayName()); } @Override public void testFinished(Description desc) { System.out.println("Finished:" + desc.getDisplayName()); } @Override public void testFailure(Failure fail) { System.out.println("Failed:" + fail.getDescription().getDisplayName() + " [" + fail.getMessage() + "]"); } }
@RunWith - аннотация говорит о том, что мы будем запускать наш тест
с помощью "запускателя" Parametrized.
Started:getSumTest[0](TestCalc)
Finished:getSumTest[0](TestCalc)
Started:getSubtractionTest[0](TestCalc)
Failed:getSubtractionTest[0](TestCalc) [expected:<2> but was:<1>]
Finished:getSubtractionTest[0](TestCalc)
Started:getSumTest[1](TestCalc)
Finished:getSumTest[1](TestCalc)
Started:getSubtractionTest[1](TestCalc)
Failed:getSubtractionTest[1](TestCalc) [expected:<5> but was:<4>]
Finished:getSubtractionTest[1](TestCalc)
Started:getSumTest[2](TestCalc)
Finished:getSumTest[2](TestCalc)
Started:getSubtractionTest[2](TestCalc)
Failed:getSubtractionTest[2](TestCalc) [expected:<-5> but was:<-6>]
Finished:getSubtractionTest[2](TestCalc)
Как видите у нас тест выполнялся три раза. Я специально разделил вывод на три части – на самом
деле получилось все вместе, но для понятности можно. В следующей части мы начнем разговор о мощной технологии – Application Server и Enterprise Java Beans. Данная технолгия потребует от Вас установки IDE NetBeans и много чего интересного. В общем читайте: Часть 11 - Application Server & Enterprise Java Beans |