Прагматичное интеграционное тестирование может повысить вашу производительность и гарантировать развертывание JavaEE-приложения.
Адам Бин (Германия) – консультант, тренер, архитектор, член экспертной группы Java EE 6 и 7, EJB 3.X, JAX-RS, и JPA 2.X JSRs. Java Champion, Top Java Ambassador 2012 и JavaOne Rock Star 2009, 2011, 2012 и 2013 гг. Автор книг «Real World Java EE Patterns–Rethinking Best Practices» и «Real World Java EE Night Hacks–Dissecting the Business Tier».
Предлагаем вам ознакомиться с переводом статьи Адама «Integration Testing for Java EE»
.
Введение
В предыдущей статье – «Unit Testing for Java EE» я сделал обзор подходов к модульному тестированию приложений на платформе Java и приложений, написанных с использованием Java EE 6 имитацией всех внешних зависимостей с использованием библиотеки Mokito. Модульные тесты важны для валидации бизнес-логики, но не гарантируют развертывание вашего приложения Java EE 6.
Примечание: На ресурсе Java.Net вы найдете Maven 3 проект для этой статьи (TestingEJBAndCDI), который был протестирован с помощью NetBeans 7 и GlassFish v3.x.
Примечание: На ресурсе Java.Net вы найдете Maven 3 проект для этой статьи (TestingEJBAndCDI), который был протестирован с помощью NetBeans 7 и GlassFish v3.x.
Использование различных подходов в тестировании
Модульные тесты – быстрые и «мелко-зернистые». Интеграционные тесты – медленные и «крупно-зернистые». Вместо использования произвольного деления модульных и интеграционных тестов на быстрые и медленные соответственно, для улучшения производительности мы будем принимать во внимание их специфику. Мелко-зернистые юнит-тесты должны выполняться быстро. Обычно тесты пишутся для маленьких частей функционала, прежде чем они будут интегрированы в более крупную подсистему. Модульные тесты невероятно быстрые – сотни тестов могут быть запущены за миллисекунды. Используя модульные тесты, возможно проводить быстрые итерации и не дожидаться выполнения интеграционных тестов.
Интеграционные тесты выполняются после успешного прохождения модульных тестов. Модульные тесты часто не срабатывают, поэтому интеграционные тесты запускаются реже. Благодаря строгому делению на модульные и интеграционные тесты, можно экономить несколько минут (или даже часов) на каждом цикле тестирования.
Тестирование для повышения производительности
Прагматичное интеграционное тестирование действительно увеличит вашу производительность. Другое дело – тестирование мэппингов и запросов Java Persistence API (JPA). Загрузка всего приложения на сервер лишь для проверки корректности синтаксиса мэппингов и запросов занимает слишком много времени.
JPA можно использовать напрямую из модульного теста. Тогда затраты на повторный запуск будут незначительными. Для этого вам достаточно получить экземпляр EntityManager из EntityManagerFactory. Например, для тестирования мэппинга класса Prediction, EntityManager внедрен в класс PredictionAudit (см. пример 1).
public class PredictionAuditIT {
private PredictionAudit cut;
private EntityTransaction transaction;
@Before
public void initializeDependencies(){
cut = new PredictionAudit();
cut.em = Persistence.createEntityManagerFactory("integration").
createEntityManager();
this.transaction = cut.em.getTransaction();
}
@Test
public void savingSuccessfulPrediction(){
final Result expectedResult = Result.BRIGHT;
Prediction expected = new Prediction(expectedResult, true);
transaction.begin();
this.cut.onSuccessfulPrediction(expectedResult);
transaction.commit();
List<Prediction> allPredictions = this.cut.allPredictions();
assertNotNull(allPredictions);
assertThat(allPredictions.size(),is(1));
}
@Test
public void savingRolledBackPrediction(){
final Result expectedResult = Result.BRIGHT;
Prediction expected = new Prediction(expectedResult, false);
this.cut.onFailedPrediction(expectedResult);
}
}
Пример 1. Внедрение EntityManager в класс PredictionAudit
Поскольку в примере 1 EntityManager работает вне контейнера, транзакции могут управляться только модульными тестами. Декларативные транзакции в данном случае недоступны. Это еще больше упрощает тестирование, так как граница транзакции может быть явно установлена внутри метода тестирования. Можно легко очистить кэш EntityManager-а, вызвав метод EntityTransaction#commit(). Сразу после очистки кэша данные становятся доступными в базе данных и могут быть проверены в процессе тестирования (см. метод savingSuccessfulPrediction() в примере 1).
Автономные конфигурации JPA
EntityManager является частью JPA-спецификации, был также включен в glassfish-embedded-all-зависимость. Такая же зависимость выполняется в EclipseLink. Вам только необходима внешняя база данных для хранения данных. База данных Derby не требует инсталляции, может запускаться в режиме сервера или в режиме встраиваемой базы данных (embedded) и может хранить данные в оперативной памяти (in-memory) или на диске.
dependency>
<groupId>org.apache.derby</groupId>
<artifactId>derbyclient</artifactId>
<version>10.7.1.1</version>
<scope>test</scope>
</dependency>
Пример 2. «Установка» базы данных Derby
Derby поддерживается стандартным репозиторием Maven и может быть добавлен в проект одной зависимостью (см. пример 2). В данном случае зависимость определена для области видимости test, поскольку драйвер JDBC нужен только во время тестирования и не должен быть развернут или установлен на сервер.
При проведении модульного тестирования без контейнера, невозможно использовать функционал Java Transaction (JTA) и javax.sql.DataSource.
<persistence version=“1.0” xmlns="#"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="#
">
<persistence-unit name="integration" transaction-type="RESOURCE_LOCAL">
<class>com.abien.testing.oracle.entity.Prediction</class>
<exclude-unlisted-classes>true</exclude-unlisted-classes>
<properties>
<property name="javax.persistence.jdbc.url" value="jdbc:derby:memory:testDB;create=true"/>
<property name="javax.persistence.jdbc.driver" value="org.apache.derby.jdbc.EmbeddedDriver"/>
<property name="eclipselink.ddl-generation" value="create-tables"/>
</properties>
</persistence-unit>
</persistence>
Пример 3. Файл persistence.xml, сконфигурированный для модульного тестирования
Дополнительный файл persistence.xml создается в пакете src/test/java/META-INF и используется исключительно для тестирования. Так как нет процесса развертывания, все сущности приходится описывать явно. Также тип транзакции установлен в RESOURCE_LOCAL, что позволяет обрабатывать транзакции вручную. Вместо декларирования источника данных, EntityManager обращается напрямую к базе данных через сконфигурированный драйвер JDBC. База данных Derby в режиме embedded лучше всего подходит для модульного тестирования. EmbeddedDriver поддерживает два варианта конфигурации URL: с хранением данных в файле или в памяти. Для тестирования JPA-отображения и запросов используется строка соединения с хранением в памяти (in-memory, см. пример 3). Все таблицы создаются на лету в оперативной памяти перед началом очередного теста и удаляются после выполнения теста. Так как нет необходимости удалять свои данные после тестирования, это самый удобный способ «дымового» тестирования JPA.
Более сложные JPA-тесты требуют определенный набор тестовых данных, и использование конфигурации in-memory для таких случаев неудобно. База данных Derby вместо оперативной памяти может использовать файлы для хранения и загрузки данных. Для этого достаточно изменить строку соединения:
<property name="javax.persistence.jdbc.url" value="jdbc:derby:./sample;create=true”/>
В частности, тесты, требующие предопределенного набора данных, могут быть легко осуществлены с использованием конфигурации базы данных с записью состояния в файл. Заполненную базу данных придется скопировать в папку проекта до выполнения теста, и она должна быть удалена после тестирования. Таким образом, база данных будет удаляться после каждого запуска. Нет необходимости беспокоиться об очистке или каких-либо модификациях.
Модульный тест – это не интеграционный тест
Вы, наверное, уже заметили странный IT-суффикс в названии класса PredictionAuditIT. Суффикс используется для отличия модульных тестов от интеграционных. Стандартный плагин failsafe фреймворка Maven выполняет все тесты, заканчивающиеся на IT или ITCase, или начинающиеся с IT, но эти классы игнорируются Maven плагином Surefire и JUnit-тестами. Вам достаточно добавить следующую зависимость, чтобы разделить модульные и интеграционные тесты:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>2.7.1</version>
</plugin>
Пример 4. Конфигурация плагина FailSafe
Модульные тесты выполняются во время стандартного исполнения mvn clean install, также их можно явно запустить с помощью команды mvn surefire:test. Интеграционные тесты могут быть также добавлены к фазам Maven с помощью тега execution:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>2.7.1</version>
<executions>
<execution>
<id>integration-test</id>
<phase>integration-test</phase>
<goals>
<goal>integration-test</goal>
</goals>
</execution>
<execution>
<id>verify</id>
<phase>verify</phase>
<goals>
<goal>verify</goal>
</goals>
</execution>
</executions>
</plugin>
Пример 5. Регистрация плагина Failsafe
При использовании тега execution плагин Failsafe начинает автоматически выполняться командами mvn install, mvn verify или mvn integration-test. Сначала выполняются модульные тесты, а потом уже интеграционные.
Строгое разделение на интеграционные и юнит-тесты значительно ускоряет цикл тестирования. Таким образом, модульные тесты проверяют функциональность метода в имитированной среде, и они на несколько порядков быстрее, чем интеграционные тесты. Мгновенный результат получается после выполнения модульных тестов.
Только успешное прохождение всех модульных тестов запускает изначально более медленные интеграционные тесты. Также возможно настраивать Continuous Integration отдельно для модульного и интеграционного тестирования. Команда mvn clean install запускает в работу команду mvn integration-tests. Разделение процессов дает больше гибкости; можно перезапускать задания отдельно и получать уведомления о процессе выполнения каждого задания.
«Убийственный» сценарий использования для встроенных интеграционных тестов
Большинство интеграционных тестов можно выполнить без запуска контейнера. JPA-функциональность может быть протестирована с помощью EntityManager, созданным локально. Бизнес-логику тоже можно удобно и эффективно протестировать вне контейнера. Для необходимо создать mock-объекты для всех зависимых классов, сервисов и всего остального.
Давайте предположим, что мы должны вывести наружу два сообщения об ошибках и держать их в одном месте для удобства поддержки. Экземпляры класса String можно внедрить и использовать для конфигурирования (см. пример 6).
@Inject
String javaIsDeadError;
@Inject
String noConsultantError;
//…
if(JAVA_IS_DEAD.equals(prediction)){
throw new IllegalStateException(this.javaIsDeadError);
}
//…
if(company.isUnsatisfied()){
throw new IllegalStateException(this.noConsultantError);
Пример 6. Возбуждение исключений с использованием внедренных строк
Класс MessageProvider поддерживает конфигурирование и возвращает строку, используя значения свойств (см. пример 7).
@Singleton
public class MessageProvider {
public static final String NO_CONSULTANT_ERROR = "No consultant to ask!";
public static final String JAVA_IS_DEAD_MESSAGE = "Please perform a sanity / reality check";
private Map<String,String> defaults;
@PostConstruct
public void populateDefaults(){
this.defaults = new HashMap<String, String>(){{
put("javaIsDeadError", JAVA_IS_DEAD_MESSAGE);
put("noConsultantError", NO_CONSULTANT_ERROR);
}};
}
@Produces
public String getString(InjectionPoint ip){
String key = ip.getMember().getName();
return defaults.get(key);
}
}
Пример 7. Общий конфигуратор
Если посмотреть на класс MessageProvider (пример 7), то видно, что не осталось ничего, что надо протестировать. Разве что можно создать mock-объект для параметра InjectionPoint поля Map defaults, чтобы протестировать поиск. Единственной непротестированной частью после внедрения зависимости (dependency injection) остается само внедрение зависимостей и использование их в методе getString(). Получение имени поля, поиска и само внедрение могут быть полноценно протестированы только внутри контейнера. Цель внедрения – класс OracleResource, который использует внедренные значения для создания исключений. (см. статью "Unit Testing for Java EE.") Для того чтобы протестировать данный механизм, придется или вывести внедренные значения, или извлечь сообщение из исключений. С таким подходом не получится протестировать «острые моменты», например, несконфигурированные поля. Вспомогательный класс, созданный исключительно для целей тестирования, даст больше гибкости и значительно упростит тестирование.
public class Configurable {
@Inject
private String shouldNotExist;
@Inject
private String javaIsDeadError;
public String getShouldNotExist() {
return shouldNotExist;
}
public String getJavaIsDeadError() {
return javaIsDeadError;
}
}
Пример 8. Вспомогательный класс для интеграционного тестирования
Класс Configurable (см. пример 8) находится в папке src/test/java и был специально разработан для упрощения интеграционного тестирования для класса MessageProvider. Предполагается, что поле javaIsDeadError будет внедрено, а поле shouldNotExist не нужно конфигурировать, и в процессе тестирования оно будет иметь значение null.
«Чужие» могут помочь вам
Arquillian – это интересное существо неземного происхождения (см. фильм «Люди в черном»), но в то же время это еще и фреймворк с открытым кодом, интегрированный в класс TestRunner фремворка JUnit. С его помощью у вас появляется возможность полного контроля над тем, какие классы развертываются и внедряются. Arquillian выполняет JUnit-тесты и имеет полный доступ к содержимому папки src/test/java. Специальные классы для тестирования, такие как Configurable (см. пример 8), можно использовать для упрощения тестирования, при этом тесты не попадут в папку с основным кодом приложения src/main/java, и нет необходимости включать их в поставку.
Arquillian состоит из двух частей: управляющей части и контейнера. Управляющая часть arquillian-junit выполняет тесты и реализует механизм внедрения зависимостей внутри тест-кейса.
<dependency>
<groupId>org.jboss.arquillian</groupId>
<artifactId>arquillian-junit</artifactId>
<version>1.0.0.Alpha5</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jboss.arquillian.container</groupId>
<artifactId>arquillian-glassfish-embedded-3.1</artifactId>
<version>1.0.0.Alpha5</version>
<scope>test</scope>
</dependency>
Пример 9. Конфигурация Arquillian для Maven 3
Зависимость arquillian-glassfish-embedded-3.1 интегрируется с сервером приложений. Вы можете использовать как версию 3.1, так и 3.0.
Вместо glassfish можно использовать встроенный сервер JBoss, используя артефакт arquillian-jbossas-embedded-6, или серверыTomcat и Weld.
import org.jboss.shrinkwrap.api.*;
import javax.inject.Inject;
import org.jboss.arquillian.junit.Arquillian;
import org.junit.*;
import static org.junit.Assert.*;
import static org.hamcrest.CoreMatchers.*;
@RunWith(Arquillian.class)
public class MessageProviderIT {
@Inject
MessageProvider messageProvider;
@Inject
Configurable configurable;
@Deployment
public static JavaArchive createArchiveAndDeploy() {
return ShrinkWrap.create(JavaArchive.class, "configuration.jar").
addClasses(MessageProvider.class, Configurable.class).
addAsManifestResource(
new ByteArrayAsset("<beans/>".getBytes()),
ArchivePaths.create("beans.xml"));
}
@Test
public void injectionWithExistingConfiguration() {
String expected = MessageProvider.JAVA_IS_DEAD_MESSAGE;
String actual = configurable.getJavaIsDeadError();
assertNotNull(actual);
assertThat(actual,is(expected));
}
@Test
public void injectionWithMissingConfiguration(){
String shouldNotExist = configurable.getShouldNotExist();
assertNull(shouldNotExist);
}
Пример 10. Интеграционные тесты с применением Arquillian
После конфигурации зависимостей, можно использовать Arquillian в качестве системы управления тестами. Модульные тесты выполняются в Arquillian прозрачно. Maven, Ant или даже ваш IDE будут просто использовать Arquillian вместо «родной» системы выполнения тестов. Хотя это перенаправление является прозрачным, оно позволяет Arquillian внедрять развернутые компоненты Contexts and Dependency Injection (CDI) или Enterprise JavaBeans (EJB), или другие ресурсы Java EE непосредственно в ваши тесты. Arquillian – это всего лишь тонкий слой над реализацией сервера приложений. Он скрыто загружает GlassFish, JBoss или Tomcat и выполняет тесты на «родном» встроенном сервере приложений. Например, он может использовать Embedded GlassFish.
Для начала использования Arquillian нам нужно задеплоить JAR-архив. В методе createArchiveAndDeploy (см. пример 10) создается и деплоится JAR с классами MessageProvider и Configurable и с пустым файлом beans.xml. Развернутые классы могут быть напрямую внедрены в сам модульный тест, что сильно упрощает тестирование. Метод injectionWithExistingConfiguration обращается к классу Configurable, который возвращает значения javaIsDeadError и shouldNotExist (см. пример 8). Можно протестрировать код так, как будто он был запущен внутри сервера приложений. С одной стороны, это лишь иллюзия, с другой стороны, нет, ведь тесты на самом деле запускают контейнер.
Тестирование невозможного
Сложно предсказать, что произойдет, если зависимость Instance Consultant> company не удовлетворена. Это не может случиться в реальной эксплуатации, так как в это время всегда развернуто достаточно реализаций класса Consultant. Эта ситуация делает невозможным тестирование в интеграционной среде. Но Arquillian позволяет легко протестировать неудовлетворенные и неопределенные зависимости, управляя модулем развертывания.
@RunWith(Arquillian.class)
public class OracleResourceIT {
@Inject
OracleResource cut;
@Deployment
public static JavaArchive createArchiveAndDeploy() {
return ShrinkWrap.create(JavaArchive.class, "oracle.jar").
addClasses(OracleResource.class,MessageProvider.class, Consultant.class).
addAsManifestResource(
new ByteArrayAsset("<beans/>".getBytes()),
ArchivePaths.create("beans.xml"));
}
@Test(expected=IllegalStateException.class)
public void predictFutureWithoutConsultants() throws Exception{
try {
cut.predictFutureOfJava();
} catch (EJBException e) {
throw e.getCausedByException();
}
}
}
Пример 11. Тестирование невозможного
Только самые необходимые классы OracleResource, MessageProvider и Consultant помещены в архив oracle.jar методом createArchiveAndDeploy из примера 11. Хотя класс OracleResource также использует Event, чтобы отправить Result (см. пример 12), мы игнорируем событие PredictionAudit и не деплоим его.
@Path("javafuture")
@Stateless
public class OracleResource {
@Inject
Instance<Consultant> company;
@Inject
Event<Result> eventListener;
@Inject
private String javaIsDeadError;
@Inject
private String noConsultantError;
// бизнес-логика опущена
}
Пример 12. Необходимые зависимости класса OracleResource.
Предполагаем, что событие Event будет проглочено. Метод predictFutureWithoutConsultants вызывает метод OracleResource#predictFutureOfJava и ожидает выброса исключения IllegalStateException после проверки условия в checkConsultantAvailability (см. пример 13).
public String predictFutureOfJava(){
checkConsultantAvailability();
Consultant consultant = getConsultant();
Result prediction = consultant.predictFutureOfJava();
eventListener.fire(prediction);
if(JAVA_IS_DEAD.equals(prediction)){
throw new IllegalStateException(this.javaIsDeadError);
}
return prediction.name();
}
void checkConsultantAvailability(){
if(company.isUnsatisfied()){
throw new IllegalStateException(this.noConsultantError);
}
}
Пример 13. Проверка предусловием в классе OracleResource
Обратите внимание, что выбрасывается исключение javax.ejb.EJBException вместо ожидаемого IllegalStateException (см. пример 14).
ПРЕДУПРЕЖДЕНИЕ: Системное исключение произошло во время вызова на стороне EJB.
class OracleResource
public java.lang.String com.abien.testing.oracle.boundary.OracleResource.predictFutureOfJava()
javax.ejb.EJBException
//опускаем несколько строк класса stacktrace…
at $Proxy126.predictFutureOfJava(Unknown Source)
at com.abien.testing.oracle.boundary.__EJB31_Generated__OracleResource__ __Intf____Bean__.predictFutureOfJava(Unknown Source)
Пример 14. Внедрение класса Stack Trace с помощью EJB Proxy.
Модульный тест обращается к реальному EJB 3.1 компоненту через public-интерфейс. IllegalStateException – это непроверяемое исключение, которое приводит к откату текущей транзакции, и его действие оборачивается в исключение javax.ejb.EJBException. Приходится получать исключение IllegalStateException из EJBException и повторно выбрасывать его.
Тестирование Rollback
Поведение в случае отката транзакции может быть легко протестировано введением вспомогательного класса: TransactionRollbackValidator. Это обычный компонент EJB 3.1 с доступом к SessionContext.
@Stateless
public class TransactionRollbackValidator {
@Resource
SessionContext sc;
@EJB
OracleResource os;
public boolean isRollback(){
try {
os.predictFutureOfJava();
} catch (Exception e) {
//swallow all exceptions intentionally
//намеренно проглатываем все исключения
}
return sc.getRollbackOnly();
}
}
Пример 15. EJB 3.1 компонент для тестирования
Тест-класс TransactionRollbackValidator вызывает класс OracleResource, обрабатывает все исключения и возвращает текущий статус отката транзакции (rollback). Приходится только слегка расширить секцию развертывания и тестирования класса OracleResourceIT, чтобы проверить успешность отката транзакции (см. пример 16).
@RunWith(Arquillian.class)
public class OracleResourceIT {
// другие исключения опущены
@Inject
TransactionRollbackValidator validator;
@Deployment
public static JavaArchive createArchiveAndDeploy() {
return ShrinkWrap.create(JavaArchive.class, "oracle.jar").
addClasses(TransactionRollbackValidator.class,
OracleResource.class,MessageProvider.class, Consultant.class).
addAsManifestResource(
new ByteArrayAsset("<beans/>".getBytes()),
ArchivePaths.create("beans.xml"));
}
@Test
public void rollbackWithoutConsultants(){
assertTrue(validator.isRollback());
}
//модульный тест, о котором говорили ранее, опускаем
}
Пример 16. Тестируем откат транзакции
TransactionRollbackValidator – это компонент EJB 3.1, поэтому он по умолчанию развернут с использованием атрибута транзакции Required. Согласно спецификации EJB, метод TransactionRollbackValidator#isRollback всегда выполняется в рамках транзакции. Либо запускается новая транзакция, либо повторно используется существующая. В нашем тест-кейсе TransactionRollbackValidator вызывает новую транзакцию класса OracleResource. Вновь запущенная транзакция распространяется на класс OracleResource, который выбрасывает EJBException по причине отсутствия консультантов. Класс TransactionRollbackValidator просто возвращает результат вызова метода SessionContext#getRollbackOnly.
Такой тест был бы невозможен без модификации исходного кода или без введения дополнительных вспомогательных классов в папку src/main/java. Такие фреймворки, как Arquillian, предоставляют уникальную возможность легко протестировать инфраструктуру, не засоряя исходный код кодом для тестирования.
…и непрерывное развертывание?
Тестирование кода снаружи и внутри контейнера гарантирует правильное поведение вашего приложения только при четко определенных условиях работы. Даже небольшое изменение конфигурации сервера или забытый ресурс нарушит развертывание. Все современные серверы приложений настраиваются либо через скрипты, либо через урезанный API. Запуск сервера из конфигурации в вашем исходном коде не только минимизирует возможные ошибки, но также сделает ручное вмешательство излишним. Можно даже выполнять развертывание приложения на промышленных серверах при каждом изменении кода в репозитории. Всесторонние интеграционные и модульные тесты являются первым необходимым условием для «непрерывного развертывания».
Злоупотребление вредит производительности
Тестирование всей функциональности внутри встроенного (embedded) контейнера удобно, но непродуктивно. Компоненты Java EE 6 являются аннотированными объектами Plain Old Java Objects (объекты POJO) и могут быть легко протестированы и мокированы с помощью инструментов Java SE, таких как JUnit, TestNG или Mockito. Использование встроенного контейнера для проверки бизнес логики не только непродуктивно, но и концептуально неправильно. Модульные тесты должны проверять только бизнес логику, а не поведение контейнера. Кроме того, большинство интеграционных тестов может быть легко запущено с использованием локального EntityManager-а (см. пример 1) или имитацией контейнерной инфраструктуры. Только для небольшой части интеграционных тестов следует использовать встроенные контейнеры или тестовые фреймворки, такие как Arquillian. И хотя такие тесты – это лишь часть общей интеграционной тестовой базы, эти тесты покрывают крайне важные случаи. Без встраиваемого контейнера невозможно проверить бизнес-логику, зависимую от инфраструктуры, не перенастраивая интеграционную среду и не загрязняя исходный код вспомогательным кодом для тестирования.
17 и 19 февраля пройдет вебинар JavaOne Rock Star Адама Бина «Тестирование, развёртывание, автоматизация и качество кода приложений на платформе Java EE 7»
Автор: Evgenia_s5