Pull to refresh

Использование связки HSQLDB+DBUnit для Unit-тестирования Java-кода, работающего с базами данных

Java *
Sandbox

Предисловие


Считается, что unit-тесты не должны использовать реальных объектов (т.е. подключений к базам данных, сетевых сокетов и подобного рода ресурсов). На этой почве развилось очень много холиваров — надо тестировать код, работающий с базами данных, или это плохой тон. Если тестировать, то можно ли это называть Unit-тестирование или это функциональное тестирование (или интеграционное, т.к. мы тестируем совместную работу двух программных сред/модулей). Споры и баталии не утихают. Я же попрошу читателей не отвлекаться на священные войны, а принять этот материал как пищу для размышления. Давайте не будем забывать, что описанное мною лишь инструмент, его применимость определяется задачей.

Выбор инструментов


Пожалуй самое сложное в Unit-тестировании — это проверка кода, работающего с подключениями к базам данных (по большому счету, проверка кода, работающего с внешними объектами). Да, можно использовать mock'и взамен подключений, но если у вас более одной операции с JDBC-провайдером, то вы с большей вероятностью сделаете ошибку в mock-объекте, чем отловите ее в коде при помощи последнего. Что же остается? Использовать реальные базы данных тоже нехорошо, ведь сервер БД в репозиторий не положишь… А если я скажу, что очень даже положишь, и он уже там находится? Решение нашей проблемы — HSQLDB.

HSQLDB — это реляционная база данных полностью написанная на Java. При этом, что очень примечательно, сервер базы данных может подниматься как отдельным инстансом, так и создаваться внутри Java-приложения. Небольшой размер и возможность полностью хранить всю базу данных в памяти (по умолчанию) делают HSQLDB идеальным сервером БД для Unit-тестирования. С учетом того, что с точки зрения JDBC и ORM реализация СУБД не имеет значения (если вы придерживаетесь стандарта SQL и не злоупотребляете расширениями движков СУБД), то мы с легкостью сможем подменить подключение к PostgreSQL или Oracle на соединение с HSQLDB при Unit-тестировании.

Хорошо, предположим, что у нас есть база данных, полностью размещающаяся в памяти и потребляющая минимальное количество ресурсов. Перед проведением тестов ее нужно заполнить данными, и желательно это делать методом более универсальным, чем написание SQL-запросов. Нам так же нужно проверять состояние базы данных после проведения операций над ней. Получать из нее данные и сверять их с эталоном вручную, согласитесь, идея крайне плохая. Поэтому, для решения проблемы инициализации и проверки результатов операции была создана библиотека DBUnit, идеально подходящая для автоматизации инициализации базы данных и последующей сверки наборов данных.

Пример использования


Для демонстрации возможностей связки HSQLDB и DBUnit создадим класс, конструктор которого принимает в качестве параметров коннектор к базе данных. У класса будет метод, принимающий в качестве параметров строку текста, разбивающий ее на отдельные слова и складывающий статистику о частоте появления слов в таблицу базы данных. Наш класс будет выглядеть следующим образом:

public class User {

	private Connection sqlConnection;
	
	public User(Connection sqlConnectopn) {
		this.sqlConnection = sqlConnectopn;
	}
	
	private int insertOriginalString(String originalString) throws SQLException {
		int originalStringId = 0;
		PreparedStatement psInsert = sqlConnection.
			prepareStatement(
				"INSERT INTO original_strings (strings, date) VALUES (?, now())",
				PreparedStatement.RETURN_GENERATED_KEYS
			);
		psInsert.setString(1, originalString);
		psInsert.execute();
		ResultSet rsInsert = psInsert.getGeneratedKeys();
		if(rsInsert.next()) {
			originalStringId = rsInsert.getInt(1);
		}
		else {
			throw new RuntimeException();
		}
		rsInsert.close();
		psInsert.close();
		return originalStringId;
	}
	
	private int insertToken(int originalStringId, String token) throws SQLException {
		int tokenId = 0;
		PreparedStatement psTokenId = sqlConnection.
				prepareStatement("SELECT id FROM tokens WHERE word = ?");
		psTokenId.setString(1, token);
		ResultSet rsToken = psTokenId.executeQuery();
		if(rsToken.next()) {
			tokenId = rsToken.getInt(1);
		}
		else {
			PreparedStatement psInsertToken = sqlConnection.
					prepareStatement(
							"INSERT INTO tokens (word) VALUES (?)", 
							PreparedStatement.RETURN_GENERATED_KEYS
					);
			psInsertToken.setString(1, token);
			psInsertToken.execute();
			ResultSet rsInserToken = psInsertToken.getGeneratedKeys();
			if(rsInserToken.next()) {
				tokenId = rsInserToken.getInt(1);
			}
			else {
				throw new RuntimeException();
			}
			rsInserToken.close();
			psInsertToken.close();
		}
		rsToken.close();
		psTokenId.close();
		return tokenId;
	}
	
	private void linkTokenToString(int originalStringId, int tokenId) throws SQLException {
		PreparedStatement psCreateLink = sqlConnection.
			prepareStatement("INSERT INTO links (original_string_id, token_id) VALUES(?,?)");
		psCreateLink.setInt(1, originalStringId);
		psCreateLink.setInt(2, tokenId);
		psCreateLink.execute();
	}
	
	public void logRequestString(String requestString) throws SQLException {
		String preParsed = requestString.replaceAll("\\W+", " ");
		String[] tokens = preParsed.split(" ");
		if(tokens.length > 0) {
			int originalStringId = insertOriginalString(requestString);
			for(String token: tokens) {
				linkTokenToString(
						originalStringId, 
						insertToken(originalStringId, token)
				);
			}
		}
	}
}


Теперь напишем unit-тест для него.

public class UserTest {

	private IDatabaseTester tester = null;
	
	@Before public void instantiate() throws Exception {
		//Creating databse server instance
		tester = new JdbcDatabaseTester("org.hsqldb.jdbcDriver", "jdbc:hsqldb:mem:" + UUID.randomUUID().toString(), "sa", "");
		//Creating tables
		tester.getConnection().getConnection().prepareStatement("CREATE SEQUENCE SEQU AS INTEGER START WITH 0").execute();
		tester.getConnection().getConnection().prepareStatement("CREATE SEQUENCE SEQU2 AS INTEGER START WITH 0").execute();
		tester.getConnection().getConnection().prepareStatement("CREATE SEQUENCE SEQU3 AS INTEGER START WITH 0").execute();
		tester.getConnection().getConnection().prepareStatement("CREATE TABLE TOKENS(ID INT GENERATED BY DEFAULT AS SEQUENCE SEQU NOT NULL PRIMARY KEY, WORD LONGVARCHAR NOT NULL)").execute();
		tester.getConnection().getConnection().prepareStatement("CREATE TABLE ORIGINAL_STRINGS(ID INT GENERATED BY DEFAULT AS SEQUENCE SEQU2 NOT NULL PRIMARY KEY, STRINGS LONGVARCHAR NOT NULL,DATE TIMESTAMP NOT NULL)").execute();
		tester.getConnection().getConnection().prepareStatement("CREATE TABLE LINKS(ID INT GENERATED BY DEFAULT AS SEQUENCE SEQU3 NOT NULL PRIMARY KEY,TOKEN_ID INT NOT NULL,ORIGINAL_STRING_ID INT NOT NULL)").execute();
		//Setting DATA_FACTORY, so DBUnit will know how to work with specific HSQLDB data types
		tester.getConnection().getConfig().setProperty(DatabaseConfig.PROPERTY_DATATYPE_FACTORY, new HsqldbDataTypeFactory());
		//Getting dataset for database initialization 
		IDataSet dataSet = new FlatXmlDataSetBuilder().build(this.getClass().getClassLoader().getResourceAsStream("template_set.xml"));
		//Initializing database
		tester.setDataSet(dataSet);
		tester.onSetup();
	}
	
	@Test
	public void logRequestStringTest() throws SQLException, Exception {
		User man = new User(tester.getConnection().getConnection());
		man.logRequestString("Hello, world!");
		ITable template = new FlatXmlDataSetBuilder().build(this.getClass().getClassLoader().
				getResourceAsStream("check_set.xml")).getTable("tokens");
		ITable actual = DefaultColumnFilter.includedColumnsTable(tester.getConnection().createDataSet().getTable("tokens"),
				template.getTableMetaData().getColumns());
		Assertion.assertEquals(template, actual);
	}
	
}


Файлы наборов данных выглядят следующим образом:

template_set.xml
<dataset>
</dataset>


check_set.xml
<tokens WORD="Hello" />
<tokens WORD="world" />


При просмотре unit-теста может сразу возникнуть вопрос: «Почему код создания таблиц в базе включен в unit-test?! Обещали же наборы данных загружать из файлов?». Да, все верно, наборы загружаем из файлов, но структуру базы описать с помощью xml и заставить это работать со всеми драйверами баз данных — процесс не из легких, ввиду разнящегося синтаксиса DDL запросов для каждой СУБД. Поэтому такая функциональность отсутствует у DBUnit.
Хочу заострить ваше внимание на следующей конструкции:
ITable actual = DefaultColumnFilter.includedColumnsTable(tester.getConnection().createDataSet().getTable("tokens"),
				template.getTableMetaData().getColumns());

Функция DefaultColumnFilter производит фильтрацию колонок, для сравнения набора данных не учитывая id записей.

Заключение


В статье я разобрал самый простой пример работы с базой данных. Читатель легко домыслит, что такой подход к тестированию применим не только при «голом» использовании ODBC-соединений, но и для ORM-фреймворков. Стабильного вам кода!
Tags:
Hubs:
Total votes 24: ↑22 and ↓2 +20
Views 21K
Comments Comments 13