Автоматизированное тестирование. Java + Spring

Рассмотрим подход к написанию модульных и интеграционных тестов для веб-приложения, написанного на языке Java с применением Spring Framework 3.1.x.
Автоматические тесты будем писать с использованием библиотек: JUnit, Mockito.

Тестируемая система

Наше тестируемое приложение осуществляет управление изображениями и реализует 4 функции:
  • загрузка изображения,
  • удаление изображения,
  • получение списка изображений
  • и получение бинарника файла образа для вывода на экран.
Каждое изображение (класс Image) состоит из наименования, превью и непосредственно образа.
Причем файлы (класс 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()));
        }
    }
    

    Видео материал

    Полный рассказ о тестировании всех функций приложения доступен на видео.