Этот блог — больше учебник, где мы описываем разработку простого модуля доступа к данным, больше для развлечения и обучения, чем что-либо еще. Весь код можно найти здесь для тех, кто не хочет печатать: 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, чтобы получить модуль доступа к данным, который вы можете использовать в других приложениях.