Эндрю Гловер
Тестирование производительности в процессе разработки приложений обычно откладывается на последний момент, и не потому, что оно не является важным, а потому, что довольно сложно выполнить эффективное тестирование со многими неизвестными переменными. В этом месяце Эндрю Гловер в серии "В погоне за качеством" рассмотрит пример тестирования производительности в составе процесса разработки и покажет два простых способа тестирования.
При разработке приложений проверка производительности приложений практически всегда остается на втором плане. Обратите внимание - проверка производительности приложений. Производительность приложения всегда остается главной составляющей, а проверка редко включается в цикл разработки.
Тестирование производительности обычно откладывается на конечные стадии разработки по различным причинам. По моему опыту, предприятия не включают в процесс разработки тестирование производительности, поскольку не знают, что ожидать от приложения в процессе разработки. Количественные показатели отбрасываются, а предположения делаются на основе ожидаемой загрузки.
Как правило, тестирование производительности выходит на передний план после одного из следующих событий:
- В процессе разработки возникают значительные проблемы с производительностью;
- Заказчик или потенциальный клиент запрашивает данные о производительности до согласия заплатить деньги.
В этом месяце будут представлены два простых способа тестирования производительности, применяемых до возникновения вышеуказанных ситуаций.
Узнать базовые низкоуровневые показатели производительности на начальных стадиях разработки программного обеспечения можно просто с помощью JUnit. Среда JUnitPerf позволяет быстро выполнять тесты с простой нагрузкой и даже выполнять тестирование с нагрузкой.
С помощью JUnitPerf можно создать два типа тестирования: TimedTest
и LoadTest
. Оба типа основаны на шаблоне проектирования Decorator и используют механизм suite
среды JUnit. TimedTest
создает предел верхнего уровня для тестирования, если предел превышается, тест считается неудачным. LoadTest
действует совместно с таймерами и создает искусственную нагрузку для отдельного теста с помощью его выполнения нужное количество раз с интервалами, задаваемыми таймером.
JUnitPerf TimedTest
позволяет создавать тесты со связанным порогом времени. При превышении порога тест считается неудачным (даже если логика тестирования выполняется успешно). Среди прочего, тесты с синхронизацией полезно использовать для выяснения и контроля показателей производительности для важных бизнес-методов. Можно даже создавать более подробные тесты и тестировать ряд методов для обеспечения их соответствия определенному временному порогу.
Например, представим приложение с элементами для ввода данных, в котором некоторые важные бизнес-методы, например, метод createWidget()
, строго зависят от порогов производительности. Вероятно, потребуется выполнить тестирование производительности функционального аспекта выполнения метода create()
. Как правило, это выясняется на последних этапах разработки разными группами с помощью различных инструментов, которые обычно не содержат методов точного определения. Предположим, вместо этого решено попробовать методологию раннего и частого тестирования .
Создание TimedTest
начинается с обычного тестирования JUnit. Другими словами TestCase
или его производная дополняется, создается метод, начинающийся с test
(см. листинг 1):
Листинг 1. Простое тестирование графического элемента интерфейса
public class WidgetDAOImplTest extends TestCase {
private WidgetDAO dao;
public void testCreate() throws Exception{
IWidget wdgt = new Widget();
wdgt.setWidgetId(1000);
wdgt.setPartNumber("12-34-BBD");
try{
this.dao.createWidget(wdgt);
}catch(CreateException e){
TestCase.fail("CreateException thrown creating a Widget");
}
}
protected void setUp() throws Exception {
ApplicationContext context =
new ClassPathXmlApplicationContext("spring-config.xml");
this.dao = (WidgetDAO) context.getBean("widgetDAO");
}
}
|
Поскольку JUnitPerf представляет собой среду на основе Decorator, для ее фактического использования необходимо предоставить метод suite()
и добавить в существующее тестирование TimedTest
. Для TimedTest
используются те же параметры, что и для Test
, а также такой же интервал времени для выполнения теста.
Также имеется возможность передать флаг boolean
в качестве третьего аргумента (false
), который приводит к быстрому неудачному завершению тестирования, то есть, если превышено максимально допустимое время, JUnitPerf немедленно считает тестирование неудачным. В противном случае тестирование выполняется до полного завершения и признается как неудачное. Различие довольно тонкое: выполнение тестирования без дополнительного флага позволяет оценить общее время, даже в случае неудачного теста. Передача значения false
, однако, означает, что общее время выполнения предоставляться не будет.
Например, в листинге 2 выполняется метод testCreate()
с двухсекундным потолком. Если во время выполнения общее время превысит это значение, тестирование считается неудачным. Дополнительный параметр boolean
не передается, поэтому тестирование выполняется полностью, как долго бы оно не длилось.
Листинг 2. Метод suite, реализованный для создания TimedTest
public static Test suite() {
long maxElapsedTime = 2000; //2 seconds
Test timedTest = new TimedTest(
new WidgetDAOImplTest("testCreate"), maxElapsedTime);
return timedTest;
}
|
Данное тестирование обычно выполняется в среде JUnit, существующие задачи Ant, претенденты Eclipse и т.д запускают это тестирование, как и любой другой тест JUnit. Единственное отличие заключается в том, что данное тестирование выполняется в контексте таймера.
В противоположность проверки временного порога для метода (или ряда методов) в сценарии тестирования в JUnitPerf используется тестирование с нагрузкой. Как и при TimedTest
, тестирование LoadTest
среды JUnitPerf's действует как decorator, включая JUnit Test
с дополнительной поточной информацией для моделирования нагрузки.
С помощью LoadTest
можно задать количество имитируемых пользователей (то есть потоков) и даже обеспечить механизм синхронизации для запуска потоков. В JUnitPerf имеется два типа Timer
: ConstantTimer
и RandomTimer
. Предоставляя оба эти типа для LoadTest
, можно более реалистично смоделировать пользовательскую нагрузку. Без Timer
все потоки запускаются одновременно.
В листинге 3 представлено тестирование с нагрузкой для 10 моделируемых пользователей, реализованное с помощью ConstantTimer
:
Листинг 3. Реализация метода suite для создания тестирования с нагрузкой
public static Test suite() {
int users = 10;
Timer timer = new ConstantTimer(100);
return new LoadTest(
new WidgetDAOImplTest("testCreate"),
users, timer);
}
|
Обратите внимание, что метод testCreate()
запускается 10 раз, каждый поток запускается с задержкой в 100 миллисекунд. Пороговых значений не задано. Эти методы просто выполняются до конца, в случае сбоя выполнения JUnit создает соответствующий отчет об ошибке.
Шаблоны Decorator не ограничиваются простым добавлением. Например, в Java™ во ввод/вывод можно добавить FileInputStream
с InputStreamReader
с BufferedReader
(просто запомните это: BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream("infilename"), "UTF8"))
).
Декорирование можно выполнять на нескольких уровнях и с классами TimedTest
и LoadTest
JUnitPerf. Если два класса декорируют друг друга, это приводит к некоторым непреодолимым сценариям тестирования, например, сценарий, в котором нагрузка размещается в бизнес-примере, а также применяется временной порог. Или можно объединить два предыдущих сценария тестирования :
- Поместите нагрузку в метод
testCreate()
;
- Задайте условие завершения каждого потока в пределах временного порога.
В листинге 4 показано, что произойдет, если применить указанные выше спецификации к декорированию нормального Test
методом LoadTest
, который декорируется TimedTest
:
Листинг 4. Тестирование с синхронизацией и декорированной нагрузкой
public static Test suite() {
int users = 10;
Timer timer = new ConstantTimer(100);
long maxElapsedTime = 2000;
return new TimedTest(new LoadTest(
new WidgetDAOImplTest("testCreate"), users, timer),
maxElapsedTime);
}
|
Как видно, метод testCreate()
выполняется 10 раз (каждый поток запускается с задержкой 100 миллисекунд), каждый поток должен завершаться в интервале двух секунд, иначе выполнение сценария тестирования будет считаться неуспешным.
Хотя JUnitPerf представляет собой среду тестирования производительности, задаваемые значения должны задаваться с учетом приблизительных оценок. Посколько все тесты, декорируемые JUnitPerf, выполняются в среде JUnit, добавляется служебная информация, особенно при применении фиксации. Поскольку JUnit декорирует все тесты setUp
и методом tearDown()
, необходимо учитывать время выполнения в общем контексте сценария тестирования.
Соответственно, я часто создаю тесты, использующие требуемую логику фиксации, а также выполняю контрольное тестирование для определения базовых значений. Это приблизительная оценка , но она служит в качестве базы, которую требуется добавлять к любому пороговому значению в тесте.
Например, если выполнение контрольного тестирования, декорированного логикой фиксации, использующей DbUnit, занимает 2,5 секунды, то ко всем требуемым пороговым значениям JUnitPerf необходимо добавить это время. Определить контрольное время можно с помощью эталонного теста, аналогичного представленному в листинге 5:
Листинг 5. Эталонное тестирование для JUnitPerf
public class DBUnitSetUpBenchmarkTest extends DatabaseTestCase {
private WidgetDAO dao = null;
public void testNothing(){
//should be about 2.5 seconds
}
protected IDatabaseConnection getConnection() throws Exception {
Class driverClass = Class.forName("org.hsqldb.jdbcDriver");
Connection jdbcConnection =
DriverManager.getConnection(
"jdbc:hsqldb:hsql://127.0.0.1", "sa", ");
return new DatabaseConnection(jdbcConnection);
}
protected IDataSet getDataSet() throws Exception {
return new FlatXmlDataSet(new File("test/conf/seed.xml"));
}
protected void setUp() throws Exception {
super.setUp();
final ApplicationContext context =
new ClassPathXmlApplicationContext("spring-config.xml");
this.dao = (WidgetDAO) context.getBean("widgetDAO");
}
}
|
Обратите внимание, что метод testNothing()
в листинге 5 не выполняет ничего . Его назначение заключается в определении общего времени выполнения метода setUp()
(которое задается в базе данных с помощью DbUnit).
Также следует помнить, что время тестирования изменяется в зависимости от конфигурации компьютера и программ, выполняющихся в момент запуска тестирования JUnitPerf. Я считаю, что выделение тестов JUnitPerf в отдельную категорию позволяет отделить их от обычных тестирований. Это означает, что они не выполняются при каждом запуске тестирования, как, например, проверка кода в среде CI. Я также обычно создаю задачи Ant для запуска этих тестирований только во время постановочных сценариев или в средах, где учитывается тестирование производительности.
Тестирование производительности с помощью JUnitPerf ни в коем случае не является точной наукой, но это замечательный способ определения и контроля производительности кода приложения на низком уровне на ранних стадиях разработки. К тому же эта расширенная среда JUnit на основе шаблона Decorator позволяет использовать JUnitPerf для декорирования существующих тестирований JUnit.
Только подумайте о времени, потраченном на беспокойство о том, как приложение будет работать с нагрузкой. Тестирование производительности с помощью JUnitPerf представляет собой один из способов сэкономить время на более важные вещи, при этом также обеспечивается качество кода приложения.
Ссылки по теме