Тестируемая система
Наше тестируемое приложение осуществляет управление изображениями и реализует 4 функции:- загрузка изображения,
- удаление изображения,
- получение списка изображений
- и получение бинарника файла образа для вывода на экран.
Причем файлы (класс File) превью и образов представляют собой отдельные домены.
Модель базы данных
На рисунке показана модель БД для хранения наших доменов.Диаграмма классов
На рисунке показана диаграмма основных классов тестируемой системы.Подготовка к тестированию
Рассмотрим структуру тестовых пакетов нашего приложения.test.java.module.dao.jdbc - ImageDAOJdbcImplTest.java - FileDAOJdbcImplTest.java test.java.module.service.impl - ImageServiceImplTest.java - FileServiceImplTest.java test.java.module.web - ImageControllerTest.java - FileControllerTest.java test.resources - env-test.properties //параметры подключения к СУБД test.resources.module - module-init-database.sql //sql скрипт очистки/создания тестовой БД test.resources.module.dao - BaseDAOJdbcImplTest-context //application context тестового окружения
Инициализация контекста
Для работы тестов, проверяющих код, который взаимодействует с базой данных, нам необходимо создать файл описывающий контекст нашего тестового окружения.Контекст описывается в файле BaseDAOJdbcImplTest-context.
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p" xmlns:context="http://www.springframework.org/schema/context" xmlns:jdbc="http://www.springframework.org/schema/jdbc" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd"> <bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"> <property name="locations"> <list> <value>classpath:env-test.properties</value> </list> </property> </bean> <context:component-scan base-package="module.dao" /> <context:annotation-config/> <tx:annotation-driven transaction-manager="transactionManager"/> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSourceModule"/> <qualifier value="txModule"/> </bean> <bean id="dataSourceModule" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close"> <property name="driverClass" value="${module.env.jdbc.driverClass}" /> <property name="jdbcUrl" value="${module.env.jdbc.url}" /> <property name="user" value="${module.env.jdbc.username}" /> <property name="password" value="${module.env.jdbc.password}" /> <qualifier value="dsModule"/> </bean> <jdbc:initialize-database data-source="dataSourceModule"> <jdbc:script location="${module.jdbc.init-datasource.path}"/> </jdbc:initialize-database> </beans>Теперь при инциализации контекста наша БД будет инициализироваться скриптом "module-init-database.sql".
DROP SEQUENCE "SEQ_XPK_FILE_OBJ"; CREATE SEQUENCE "SEQ_XPK_FILE_OBJ"; DROP SEQUENCE "SEQ_XPK_IMG"; CREATE SEQUENCE "SEQ_XPK_IMG"; DELETE FROM "IMAGE"; DELETE FROM "FILES";Данный скрипт приводит нашу БД к начальному состоянию при каждом запуске тестов.
Интеграционное тестирование слоя DAO
Напишем тест, который будет тестировать поведение класса FileDAOJdbcImpl.@Repository public class FileDAOJdbcImpl extends AbstractDAOJdbcImpl implements FileDAO { @Override public File createFile(String contentType, byte[] blob) { File file = new File(contentType, blob); String sql = "INSERT INTO FILES(FILE_TYPE, FILE_BLOB, FILE_SIZE) " + "VALUES(:type,:blob,:size)"; SqlParameterSource namedParameters = new BeanPropertySqlParameterSource(file); KeyHolder keyHolder = new GeneratedKeyHolder(); Integer rows = jdbcTemplate.update(sql, namedParameters, keyHolder, new String[]{"FILE_OBJ_ID"}); if (rows == 1) { file.setId(keyHolder.getKey().longValue()); return file; } return null; } @Override public File getFileById(Long id) { MapSqlParameterSource params = new MapSqlParameterSource(); params.addValue("id", id); return jdbcTemplate.queryForObject("SELECT FILE_OBJ_ID, FILE_TYPE, FILE_BLOB, FILE_SIZE FROM FILES " + "WHERE FILE_OBJ_ID=:id", params, new FileMapper()); } }Класс теста FileDAOJdbcImplTest.
public class FileDAOJdbcImplTest extends BaseDAOJdbcImplTest { @Autowired FileDAO fileDAO; Long dbFileId; public FileDAOJdbcImplTest() { } @Before @Override public void setUp() { } private Long insertFile(File file) { String sql = "INSERT INTO FILES(FILE_TYPE, FILE_BLOB, FILE_SIZE) " + "VALUES(:type,:blob,:size)"; SqlParameterSource namedParameters = new BeanPropertySqlParameterSource(file); KeyHolder keyHolder = new GeneratedKeyHolder(); Integer rows = jdbcTemplate.update(sql, namedParameters, keyHolder, new String[]{"FILE_OBJ_ID"}); return keyHolder.getKey().longValue(); } /** * Test for createFile method of class FileDAOJdbcImpl. */ @Test public void testCreateFile() { String testContentType = "image/test"; byte[] testBlob = new byte[]{1, 2, 3}; Long expId = 1L; File expFile = new File(expId, testContentType, testBlob.length, testBlob); File result = fileDAO.createFile(testContentType, testBlob); assertEquals(expFile.getId(), result.getId()); assertEquals(expFile.getSize(), result.getSize()); assertEquals(expFile.getType(), result.getType()); assertTrue(Arrays.equals(testBlob, result.getBlob())); } /** * Test for getFileById method of class FileDAOJdbcImpl. */ @Test public void testGetFileById() { String testContentType = "image/test"; byte[] testBlob = new byte[]{1, 2, 3}; Long testId = insertFile(new File(testContentType, testBlob)); File result = fileDAO.getFileById(testId); assertNotNull(result); assertEquals(testId, result.getId()); assertEquals(testBlob.length, result.getSize().intValue()); assertEquals(testContentType, result.getType()); assertTrue(Arrays.equals(testBlob, result.getBlob())); } }Данный класс наследуется от класса BaseDAOJdbcImplTest.
@Ignore @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration @TransactionConfiguration(transactionManager="transactionManagerModule", defaultRollback=true) @Transactional() public abstract class BaseDAOJdbcImplTest{ protected NamedParameterJdbcTemplate jdbcTemplate; @Autowired @Qualifier("dsModule") public void setDataSource(DataSource dataSource) { this.jdbcTemplate = new NamedParameterJdbcTemplate(dataSource); } @Before public void setUp() { } @After public void tearDown() { } }Основным предназначением класса BaseDAOJdbcImpl является агрегация в себе всех настроек, которые потом понадобятся всем тестам слоя DAO.
Остановимся на них более внимательно:
- @Ignore - аннотация обозначает, что данный класс не содержит тестов для запуска
- @RunWith(SpringJUnit4ClassRunner.class) - аннотация указывает на движок Junit, используемый для запуска тестов
- @ContextConfiguration - данная аннотация указывает спрингу на то, что перед запуском тестов необходимо создать контекст. По умолчанию xml файл контекста должен располагаться в том же пакете где и тест и называться <Класс тест>-context.xml, т.е. в нашем случае BaseDAOJdbcImplTest-context.xml.
- @TransactionConfiguration(transactionManager="transactionManagerModule", defaultRollback=true) - аннотация определяет, что после каждого теста будет производиться откат транзакции.
- @Transactional() - аннотация указывает на то, что все тесты будут выполняться в отдельных транзакциях и в нашем случае каждая транзакция будет откатываться.
Модульное тестирование контроллера
Напишем тест, который будет тестировать метод получения бинарника изображения.
Класс FileController.@Controller public class FileController { @Autowired private FileService fileService; @RequestMapping(value = "/secure/loadfile.htm") public void loadFile(HttpServletResponse response, @RequestParam(value = "id", required = true) Long id) throws IOException { File file = fileService.getFileById(id); if(file != null) { response.setContentType(file.getType()); ServletOutputStream out = response.getOutputStream(); out.write(file.getBlob()); out.flush(); out.close(); } } }
А теперь тест FileControllerTest.@RunWith(MockitoJUnitRunner.class) public class FileControllerTest { @Mock FileService fileServiceMock; @InjectMocks private FileController controller; public FileControllerTest() { } /** * Test of loadFile method, of class FileController. */ @Test public void testLoadFile() throws Exception { // given MockHttpServletResponse mockResponse = new MockHttpServletResponse(); Long testId = 1L; String testContentType = "image/jpeg"; byte[] testBlob = new byte[]{1,2,3}; File testFile = new File(testId,testContentType,testBlob.length, testBlob); when(fileServiceMock.getFileById(testId)).thenReturn(testFile); // when controller.loadFile(mockResponse, testId); // then assertEquals(testContentType, mockResponse.getContentType()); assertTrue(Arrays.equals(testBlob,mockResponse.getContentAsByteArray())); } }
Видео материал
Полный рассказ о тестировании всех функций приложения доступен на видео.