Статьи

Модуль доступа к данным с использованием Groovy с тестированием Спока

Этот блог — больше учебник, где мы описываем разработку простого модуля доступа к данным, больше для развлечения и обучения, чем что-либо еще. Весь код можно найти здесь для тех, кто не хочет печатать: https://github.com/ricston-git/tododb

В качестве хедз-апа мы рассмотрим следующее:

  • Использование Groovy в проекте Maven в Eclipse
  • Использование Groovy для взаимодействия с нашей базой данных
  • Тестирование нашего кода с использованием фреймворка Spock
  • Мы включили Spring в наши тесты с ContextConfiguration

Хорошее место для начала — написать файл pom, как показано здесь . Единственные зависимости, которые мы хотим добавить в этот артефакт, — это groovy-all и commons-lang. Другие либо будут предоставлены Tomcat, либо используются только во время тестирования (отсюда и теги областей действия в pom). Например, мы поместили бы jar с драйвером PostgreSQL в библиотеку Tomcat, а tomcat-jdbc и tomcat-dbcp уже есть. (Примечание: что касается postgre jar, нам также потребуется выполнить небольшую настройку в Tomcat, чтобы определить источник данных, который мы можем получить в нашем приложении через JNDI — но это выходит за рамки этого блога. Для получения дополнительной информации см. Здесь ). Что касается тестирования, я полагаюсь на пружинный тест, спок-сердечник и спок-пружину (последняя — получить спок для работы с пружинным тестом).

Другим важным дополнением в pom является maven-compiler-plugin. Я пытался заставить gmaven работать с Groovy в Eclipse, но я обнаружил, что с плагином maven-compiler-plugin работать намного проще.

Когда ваш pom находится в пустой директории, продолжайте и запустите mkdir -p src / main / groovy src / main / java src / test / groovy src / test / java src / main / resources src / test / resources. Это дает нам структуру каталогов в соответствии с конвенцией Maven.

Теперь вы можете продолжить и импортировать проект как проект Maven в Eclipse (установите плагин m2e, если у вас его еще нет). Важно, чтобы вы не использовали mvn eclipse: eclipse в своем проекте. Генерируемый .classpath будет конфликтовать с вашим плагином m2e и (по крайней мере, в моем случае), когда вы обновляете pom.xml, плагин не будет обновлять ваши зависимости внутри Eclipse. Так что просто импортируйте как проект maven, как только вы настроите pom.xml и структуру каталогов.

Итак, наши тесты будут интеграционными, фактически используя базу данных PostgreSQL. Так как это так, давайте настроим нашу базу данных с некоторыми данными. Сначала создайте базу данных tododbtest, которая будет использоваться только для целей тестирования. Затем поместите следующие файлы в ваш src / test / resources:

Примечание, введите ваше имя пользователя / пароль:

<?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:context="http://www.springframework.org/schema/context"
	xmlns:jdbc="http://www.springframework.org/schema/jdbc" xmlns:p="http://www.springframework.org/schema/p"
	xmlns:tx="http://www.springframework.org/schema/tx"
	xsi:schemaLocation="
		http://www.springframework.org/schema/beans
		http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
		http://www.springframework.org/schema/context
		http://www.springframework.org/schema/context/spring-context-3.0.xsd">

	<bean id="dataSource" class="org.apache.tomcat.jdbc.pool.DataSource">
		<property name="driverClassName" value="org.postgresql.Driver" />
		<property name="url" value="jdbc:postgresql://localhost:5432/tododbtest" />
		<property name="username" value="fillin" />
		<property name="password" value="fillin" />
	</bean>
</beans>

<?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:context="http://www.springframework.org/schema/context"
	xmlns:jdbc="http://www.springframework.org/schema/jdbc"
	xsi:schemaLocation="
		http://www.springframework.org/schema/beans 
		http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
		http://www.springframework.org/schema/jdbc 
		http://www.springframework.org/schema/jdbc/spring-jdbc-3.0.xsd">

<!--Intialize the database schema with test data -->
	<jdbc:initialize-database data-source="dataSource">
	   <jdbc:script location="classpath:schema.sql"/>
	   <jdbc:script location="classpath:test-data.sql"/>
	</jdbc:initialize-database>
</beans>

DROP TABLE IF EXISTS todouser CASCADE;

CREATE TABLE todouser
(
  id SERIAL,
  email varchar(80) UNIQUE NOT NULL,
  password varchar(80),
  registered boolean DEFAULT FALSE,
  confirmationCode varchar(280),
  CONSTRAINT todouser_pkey PRIMARY KEY (id)
);

insert into todouser (email, password, registered, confirmationCode) values ('[email protected]', 'abc123', FALSE, 'abcdefg')
insert into todouser (email, password, registered, confirmationCode) values ('[email protected]', 'pass1516', FALSE, '123456')
insert into todouser (email, password, registered, confirmationCode) values ('[email protected]', 'anon', FALSE, 'codeA')
insert into todouser (email, password, registered, confirmationCode) values ('[email protected]', 'anon2', FALSE, 'codeB')

<?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:context="http://www.springframework.org/schema/context"
	xmlns:jdbc="http://www.springframework.org/schema/jdbc"
	xmlns:p="http://www.springframework.org/schema/p"
	xmlns:tx="http://www.springframework.org/schema/tx"
	xsi:schemaLocation="
		http://www.springframework.org/schema/beans 
		http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
		http://www.springframework.org/schema/context 
		http://www.springframework.org/schema/context/spring-context-3.0.xsd">

	<import resource="classpath:datasource.xml"/>
	<import resource="classpath:initdb.xml"/>

</beans>

По сути, testContext.xml — это то, с чем мы будем настраивать контекст нашего теста. Подразделение на datasource.xml и initdb.xml может быть немного слишком много для этого примера … но изменения, как правило, легче сделать таким образом. Суть в том, что мы настраиваем наш источник данных в datasource.xml (это то, что мы будем внедрять в наших тестах), а initdb.xml будет запускать schema.sql и test-data.sql, чтобы создать нашу таблицу и заполнить ее. с данными.

Итак, давайте создадим наш тест, или я должен сказать, нашу спецификацию. Спок — это спецификационная структура, которая позволяет нам писать более описательные тесты. В целом, это облегчает чтение и понимание наших тестов, и, поскольку мы будем использовать Groovy, мы могли бы также использовать дополнительную удобочитаемость, которую дает нам Спок.

package com.ricston.blog.sample.model.spec;

import javax.sql.DataSource

import org.springframework.beans.factory.annotation.Autowired
import org.springframework.test.annotation.DirtiesContext
import org.springframework.test.annotation.DirtiesContext.ClassMode
import org.springframework.test.context.ContextConfiguration

import spock.lang.Specification

import com.ricston.blog.sample.model.data.TodoUser
import com.ricston.blog.sample.model.dao.postgre.PostgreTodoUserDAO


// because it supplies a new application context after each test, the initialize-database in initdb.xml is
// executed for each test/specification
@DirtiesContext(classMode=ClassMode.AFTER_EACH_TEST_METHOD)
@ContextConfiguration('classpath:testContext.xml')
class PostgreTodoUserDAOSpec extends Specification {

	@Autowired
	DataSource dataSource
	
	PostgreTodoUserDAO postgreTodoUserDAO
	
	def setup() {
		postgreTodoUserDAO = new PostgreTodoUserDAO(dataSource)
	}

	def "findTodoUserByEmail when user exists in db"() {
		given: "a db populated with a TodoUser with email [email protected] and the password given below"
		String email = '[email protected]'
		String password = 'anon'

		when: "searching for a TodoUser with that email"
		TodoUser user = postgreTodoUserDAO.findTodoUserByEmail email

		then: "the row is found such that the user returned by findTodoUserByEmail has the correct password"
		user.password == password
	}
	
}

Пока достаточно одной спецификации, просто чтобы убедиться, что все движущиеся части прекрасно работают вместе. Сама спецификация достаточно проста для понимания. Мы просто используем метод findTodoUserByEmail в PostgreTodoUserDAO, о котором мы скоро напишем. Используя ContextConfiguration из Spring Test, мы можем внедрить bean-компоненты, определенные в нашем контексте (в нашем случае это источник данных), используя аннотации. Это делает наши тесты короткими и облегчает их последующее изменение. Кроме того, обратите внимание на использование DirtiesContext. По сути, после выполнения каждой спецификации мы не можем полагаться на состояние базы данных, оставшейся нетронутой. Я использую DirtiesContext, чтобы получить новый контекст Spring для каждого запуска спецификации. Таким образом, создание таблиц и вставка тестовых данных происходит заново для каждой выполняемой нами спецификации.

Прежде чем мы сможем запустить нашу спецификацию, нам нужно создать как минимум следующие два класса, используемых в спецификации: TodoUser и PostgreTodoUserDAO

package com.sample.data

import org.apache.commons.lang.builder.ToStringBuilder

class TodoUser {
	long id;
	String email;
	String password;
	String confirmationCode;
	boolean registered;
	
	@Override
	public String toString() {
		ToStringBuilder.reflectionToString(this);
	}
}
package com.ricston.blog.sample.model.dao.postgre

import groovy.sql.Sql

import javax.sql.DataSource

import com.ricston.blog.sample.model.dao.TodoUserDAO
import com.ricston.blog.sample.model.data.TodoUser

class PostgreTodoUserDAO implements TodoUserDAO {
	private Sql sql

	public PostgreTodoUserDAO(DataSource dataSource) {
		sql = new Sql(dataSource)
	}

	/**
	 *
	 * @param email
	 * @return the TodoUser with the given email
	 */
	public TodoUser findTodoUserByEmail(String email) {
		sql.firstRow """SELECT * FROM todouser WHERE email = $email"""
	}
}
package com.ricston.blog.sample.model.dao;

import com.ricston.blog.sample.model.data.TodoUser;

public interface TodoUserDAO {

	/**
	 * 
	 * @param email
	 * @return the TodoUser with the given email
	 */
	public TodoUser findTodoUserByEmail(String email);

}

Мы просто создаем POGO в TodoUser, реализуя его toString с помощью обычного ToStringBuilder.

В PostgreTodoUserDAO мы используем SQL Groovy для доступа к базе данных, пока только реализуя метод findTodoUserByEmail. PostgreTodoUserDAO реализует TodoUserDAO, интерфейс, который определяет необходимые методы, которые должен иметь TodoUserDAO.

Хорошо, теперь у нас есть все, что нам нужно для запуска нашей спецификации. Иди вперед и запусти его как тест JUnit от Eclipse. Вы должны получить следующее сообщение об ошибке:

org.codehaus.groovy.runtime.typehandling.GroovyCastException: Cannot cast object '{id=3, [email protected], password=anon, registered=false, confirmationcode=codeA}' with class 'groovy.sql.GroovyRowResult' to class 'com.ricston.blog.sample.model.data.TodoUser' due to: org.codehaus.groovy.runtime.metaclass.MissingPropertyExceptionNoStack: No such property: confirmationcode for class: com.ricston.blog.sample.model.data.TodoUser
Possible solutions: confirmationCode
	at com.ricston.blog.sample.model.dao.postgre.PostgreTodoUserDAO.findTodoUserByEmail(PostgreTodoUserDAO.groovy:23)
	at com.ricston.blog.sample.model.spec.PostgreTodoUserDAOSpec.findTodoUserByEmail when user exists in db(PostgreTodoUserDAOSpec.groovy:37)

Идите вперед и подключитесь к вашей tododbtestбазе данных иselect * from todouser;

Как вы можете видеть, наш confirmationCode varchar(280),столбец оказался confirmationcodeв нижнем регистре «c».

В findTodoUserByEmail PostgreTodoUserDAO мы получаем GroovyRowResult из нашего вызова firstRow. GroovyRowResult реализует Map, а Groovy может создавать POGO (в нашем случае TodoUser) из Map. Однако для того, чтобы Groovy мог автоматически приводить GroovyRowResult к TodoUser, ключи на карте (или GroovyRowResult) должны соответствовать именам свойств в нашем POGO. Мы используем confirmationCodeв нашем TodoUser, и мы хотели бы придерживаться соглашения о случае верблюда. Что мы можем сделать, чтобы обойти это?

Ну, во-первых, давайте изменим нашу схему для использования confirmation_code. Это немного более читабельно. Конечно, у нас все та же проблема, что и раньше, поскольку confirmation_codeона не будет отображаться confirmationCodeсама по себе. (Примечание: не забудьте изменить операторы вставки в test-data.sql).

Один из способов обойти это — использовать методы propertyMissing Groovy, как показано ниже:

	def propertyMissing(String name, value) {
		if(isConfirmationCode(name)) {
			this.confirmationCode = value
		} else {
			unknownProperty(name)
		}
	}

	def propertyMissing(String name) {
		if(isConfirmationCode(name)) {
			return confirmationCode
		} else {
			unknownProperty(name)
		}
	}

	private boolean isConfirmationCode(String name) {
		'confirmation_code'.equals(name)
	}

	def unknownProperty(String name) {
		throw new MissingPropertyException(name, this.class)
	}

Добавляя это в наш TodoUser.groovy, мы эффективно рассказываем, как Groovy разрешает доступ к свойствам. Когда мы делаем что-то подобное user.confirmationCode, Groovy автоматически вызывает getConfirmationCode()метод, который мы получили бесплатно, когда объявили свойство confirmationCodeв нашем TodoUser. Теперь, когда user.confirmation_codeвызывается, Groovy не находит никаких получателей для вызова, поскольку мы никогда не объявляли свойство confirmation_code, однако, поскольку мы теперь реализовали методы propertyMissing, перед тем как выдавать любые исключения, они будут использовать эти методы в качестве крайней меры при разрешении свойств. В нашем случае мы эффективно проверяем, выполняются ли confirmation_codeоперации get или set , и сопоставляем соответствующие операции нашимconfirmationCodeимущество. Это так просто. Теперь мы можем сохранить автоматическое приведение в нашем объекте доступа к данным и имя свойства, которое мы выбрали для нашего TodoUser.

Предполагая, что вы внесли изменения в схему и test-data.sql, используйте confirmation_codeфайл спецификации и на этот раз он должен пройти.

Вот именно для этого урока. В заключение я хотел бы обсудить некоторые тонкости, о которых может не знать тот, кто никогда раньше не использовал SQL Groovy. Как вы можете видеть в PostgreTodoUserDAO.groovy, наше взаимодействие с базой данных в значительной степени однострочное. Как насчет обработки ресурсов (например, правильного закрытия соединения, когда мы закончим), регистрации ошибок и подготовленных операторов? Обработка ресурсов и регистрация ошибок выполняются автоматически, вам просто нужно беспокоиться о написании своего SQL. Когда вы пишете SQL, попробуйте использовать тройные кавычки, как в примере с PostgreTodoUserDAO.groovy. Это производит подготовленные операторы, поэтому защищает от внедрения SQL и избавляет нас от необходимости ставить ‘?’ повсюду и правильно выстраивая аргументы для передачи в оператор SQL.

Обратите внимание, что управление транзакциями — это то, о чем должен заботиться код, использующий наш артефакт.

Наконец, обратите внимание, что в GitHub реализован ряд других операций (кроме findTodoUserByEmail): https://github.com/ricston-git/tododb . Кроме того, есть также тест спецификации для TodoUser, который проверяет правильность отображения свойств. Кроме того, в файле pom.xml есть некоторая конфигурация maven-surefire-plugin, чтобы плагин surefire мог подобрать наши спецификации Spock, а также любые тесты JUnit, которые могут быть в нашем проекте. Это позволяет нам запускать наши спецификации, когда мы, например mvn clean package,.

После реализации всех операций, которые вам требуются в PostgreTodoUserDAO.groovy, вы можете скомпилировать jar или включить в многомодульный проект Maven, чтобы получить модуль доступа к данным, который вы можете использовать в других приложениях.