Этот блог — больше учебник, где мы описываем разработку простого модуля доступа к данным, больше для развлечения и обучения, чем что-либо еще. Весь код можно найти здесь для тех, кто не хочет печатать: 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 ('abc.j123@gmail.com', 'abc123', FALSE, 'abcdefg')
insert into todouser (email, password, registered, confirmationCode) values ('def.123@gmail.com', 'pass1516', FALSE, '123456')
insert into todouser (email, password, registered, confirmationCode) values ('anon@gmail.com', 'anon', FALSE, 'codeA')
insert into todouser (email, password, registered, confirmationCode) values ('anon2@gmail.com', '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 anon@gmail.com and the password given below"
String email = 'anon@gmail.com'
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=anon@gmail.com, 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, чтобы получить модуль доступа к данным, который вы можете использовать в других приложениях.