В преддверии старта курса "Java QA Automation Engineer" подготовили перевод полезного материала.
Также приглашаем поучаствовать в открытом вебинаре на тему «HTTP. Postman, Newman, Fiddler (Charles), curl, SOAP. SoapUI». На этом занятии участники вместе с экспертом разберут, какие бываю API и каким способом можно проверить, что backend отдает ожидаемые данные, а также познакомятся с основными инструментами для тестирования.
Бывает, и достаточно часто, что во время автоматизированного тестирования наши тесты должны взаимодействовать с базами данных. Иногда нам нужно установить какие-либо тестовые данные. В других случаях нам нужно совершать запросы в базу данных, чтобы получить те самые тестовые данные. И давайте не будем забывать об очистке данных, которые мы использовали и которые больше нам не нужны. В этой статье я покажу, как вы можете использовать класс Spring JdbcTemplate для упрощения работы с базой данных MySQL из ваших автоматизированных тестов на Java.
Вам также следует взглянуть на это замечательное дополнение к базе данных MySQL, созданное сообществом для библиотеки дополнений TestProject, позволяющее расширить возможности тестирования с помощью предварительно созданных автоматизированных экшенов, которые вы можете мгновенно внедрить в ваши написанные и закодированные тесты.
Требования
Прежде чем мы сможем начать наши взаимодействия с базой данных, нам нужно кое-что настроить. А именно зависимости, которые нам нужно добавить в наш проект. В моем случае, поскольку я использую Maven, зависимости, которые мне нужно добавить в файл pom.xml
, будут выглядеть следующим образом:
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.23</version>
</dependency>
Первая зависимость, которую мы здесь видим, — это зависимость из пакета Spring. Здесь мы можем найти класс JdbcTemplate
, который мы будем использовать для коммуникации с базой данных. Этот класс содержит полезные методы для обновления или получения данных из базы данных. Вторая зависимость требуется для связи с инстансом MySQL.
Примечание: эти зависимости имеют последнюю доступную на момент написания статьи версию (какую вы можете увидеть в репозитории Maven). Версия mysql-connector-java должна быть синхронизирована с версией инстанса MySQL, на котором работает ваша база данных. В моем случае, мой сервер MySQL имеет версию > 8, поэтому версия моего «mysql-connector-java» также выше чем 8.
Подключение к базе данных
После того как мы разобрались с зависимостями, мы можем установить связь с нашей базой данных. Мы могли бы написать код, необходимый для этой операции, в нашем тесте. Однако нам обязательно понадобится этот код и в других тестовых классах. Следовательно, этот код может быть написан либо в специальном классе для работы с базой данных, либо в базовом классе, расширяемом вашими тестами. Независимо от того, какой вариант вы выберете, соединение может быть установлено с помощью подобного метода:
public DataSource mysqlDataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://dbURL:portNumber/nameOfDB?useSSL=false");
dataSource.setUsername("username");
dataSource.setPassword("password");
return dataSource;
}
Первое, на что нам нужно обратить внимание, это то, что этот метод возвращает DataSource
. Это необходимо для инициализации класса JdbcTemplate
, который мы будем использовать в наших тестах, поскольку он хранит соединение с базой данных.
Затем в качестве имени класса драйвера в этом примере я использовал значение «com.mysql.cj.jdbc.Driver
». Опять же это требуется для установки соединения, и в некоторых случаях в более старых версиях зависимостей коннектора MySQL вместо него следует использовать «com.mysql.jdbc.Driver
». Если вы используете неправильное имя, вы получите соответствующее предупреждение при попытке подключения к базе данных.
Вам нужно будет указать расположение базы данных в методе setUrl
. Он состоит из URL-адреса, порта и имени базы данных. И, конечно же, вам необходимо указать имя пользователя и пароль для подключения к базе данных, с помощью методов setUsername
и setPassword
.
Теперь, когда соединение установлено, нам нужно инициализировать класс JdbcTemplate
. Мы можем объявить переменную этого типа в нашем тестовом классе:
private JdbcTemplate jdbcTemplate;
Затем в методе @BeforeAll
мы можем инициализировать эту переменную, предоставив соединение, которое мы установили с базой данных:
jdbcTemplate = new JdbcTemplate(nameOfClass.mysqlDataSource());
На этом настройка завершена, соединение установлено, и мы можем начать обновление (updating) или запрашивание (querying) базы данных.
Update
В классе JdbcTemplate
мы можем найти много полезных методов. Один из них, ‘update’, может быть использован для создания и обновления таблиц, добавления в них данных или даже удаления данных. Существует несколько вариантов этого метода (с разными сигнатурами), но тот, который я приведу здесь в качестве примера, принимает один параметр: SQL-запрос в виде String.
Пример
Создадим две новые таблицы: одну с именем ‘meal’ (блюдо) и ‘ingredient’ (ингредиент). В таблице ‘meal’ мы хотим хранить название блюда, присвоенную ему категорию (представляющую, будь то завтрак, обед или ужин) и автоматически сгенерированный id
в качестве первичного ключа (primary key). Для создания таблицы напишем в тестовом методе следующий код:
jdbcTemplate.update("create table meal(\n" +
" meal_id bigint auto_increment primary key,\n" +
" name varchar(50) not null unique,\n" +
" category varchar(50) not null\n" + ");");
Когда мы запустим тест, таблица будет создана. Для создания таблицы больше ничего не требуется. Допустим, мы также хотим добавить к этому столу два блюда: фахитас из курицы и энчилада. Сделать это легко — просто передадим требуемый SQL-запрос в метод update
следующим образом:
jdbcTemplate.update("insert into meal (name, category) values ('Chicken Fajita', 'lunch');");
jdbcTemplate.update("insert into meal (name, category) values ('Enchilada', 'lunch');");
Как видите, у нас по одному вызову метода update
на одну SQL-операцию.
Теперь давайте создадим таблицу под названием ingredient
. У нее не будет автоматически сгенерированного первичного ключа. Однако у нее будет внешний ключ (foreign key), соответствующий значению meal_id
из таблицы meal
. Каждая запись в этой таблице представляет собой ингредиент, соответствующий блюду из таблицы meal
. Этот внешний ключ свяжет ингредиент с блюдом. Кроме того, в таблице ingredient
есть столбцы для хранения названия ингредиента (name
), количества (quantity
) и единицы измерения (‘uom’ - unit of measure
) для количества ингредиента.
Для того чтобы создать эту таблицу, а затем добавить к ней внешний ключ, мы снова будем использовать метод update
, которому мы передадим соответствующий SQL-запрос:
jdbcTemplate.update("create table ingredient(\n" +
" meal_id bigint not null,\n" +
" name varchar(50) not null,\n" +
" quantity bigint not null,\n" +
" uom varchar(50) not null\n" + ");");
jdbcTemplate.update("alter table ingredient add foreign key (meal_id)" +
" references meal(meal_id);\n");
Чтобы иметь больше данных для наших следующих примеров, я также добавлю некоторые данные в таблицу ingredient
:
jdbcTemplate.update("insert into ingredient (meal_id, name, quantity,"
+ " uom) values ((select meal_id from meal where name = 'Chicken Fajita'), 'chicken', 1, 'kg');\n");
jdbcTemplate.update("insert into ingredient (meal_id, name, quantity, uom) " +
"values ((select meal_id from meal where name = 'Chicken Fajita'), 'red pepper', 1, 'piece');\n");
jdbcTemplate.update("insert into ingredient (meal_id, name, quantity, uom) " +
"values ((select meal_id from meal where name = 'Chicken Fajita'), 'green pepper', 1, 'piece');\n");
jdbcTemplate.update("insert into ingredient (meal_id, name, quantity, uom) " +
"values ((select meal_id from meal where name = 'Chicken Fajita'), 'yellow pepper', 1, 'piece');");
jdbcTemplate.update("insert into ingredient (meal_id, name, quantity," + " uom) " +
"values ((select meal_id from meal where name = " + "'Enchilada'), 'chicken', 1, 'kg');\n");
jdbcTemplate.update("insert into ingredient (meal_id, name, quantity," + " uom) " +
"values ((select meal_id from meal where name = " + "'Enchilada'), 'cheese', 100, 'grams');\n");
jdbcTemplate.update("insert into ingredient (meal_id, name, quantity," + " uom) " +
"values ((select meal_id from meal where name = " + "'Enchilada'), 'tomato', 1, 'piece');\n");
Отлично, у нас есть 2 таблицы с данными, которые мы можем запрашивать. Теперь же мы будем использовать разные методы из класса JdbcTemplate
для получения результатов разных типов.
queryForObject — получить одно значение
Если нам нужно запросить из базы данных одно значение, мы можем использовать метод queryForObject
. У этого метода также есть несколько вариантов использования, но здесь мы рассмотрим наиболее простой:
jdbcTemplate.queryForObject(String sqlStatement, Class returnType);
При вызове этого метода нам нужно указать, какой тип возвращаемого значения должен иметь запрос (Class
). Мы могли бы, например, получить значение String
(указав String.class
) или целое число (указав Integer.class
).
Пример
Нам нужно запросить базу данных, чтобы получить значение meal_id
из таблицы meal
для блюда ‘Chicken Fajita’. Нам нужно сохранить этот результат в переменной с типом int
:
int id = jdbcTemplate.queryForObject("select meal_id from meal
where name='Chicken Fajita';", Integer.class);
Здесь вы можете видеть, что тип возвращаемого значения запроса указан как Integer.class
, поэтому результат сохраняется в переменной с типом int
. Допустим, в тесте мы также хотим вывести в консоль результат этого запроса:
System.out.println("Meal id for Chicken Fajita = " + id);
Результатом этого вывода будет:
Meal id for Chicken Fajita = 1
queryForMap — получить строку
Теперь предположим, что вы хотите получить целую строку из таблицы. Или части строки. Вы можете сделать это с помощью метода queryForMap
, которому вы передаете необходимый SQL-запрос:
jdbcTemplate.queryForMap(String sqlStatement);
Результат этого запроса можно сохранить в переменной типа Map
. Ключи map
будут соответствовать имени каждого столбца, которому принадлежит элемент строки. Значение будет соответствовать фактическому значению из строки, соответствующей этому столбцу.
Пример
Мы хотим извлечь все данные о блюде с id 1
из таблицы ‘meal’, сохранить их в переменной и вывести результат в консоль. Это легко можно сделать следующим образом:
Map<String, Object> entireRowAsMap = jdbcTemplate.queryForMap("select * from meal where meal_id = 1");
System.out.println("All details of meal with id 1 = " + entireRowAsMap);
Как видите, переменная entireRowAsMap
представляет Map
, ключи которой — String
, а значения — Object
. Это происходит потому, что некоторые значения являются целыми числами, некоторые — строками и, конечно же, все эти типы являются объектами в Java. Вывод в консоль для приведенного выше кода:
All details of meal with id 1 = {meal_id=1, name=Chicken Fajita, category=lunch}
queryForList — получить столбец
Когда вам нужно получить либо все значения, либо часть значений из конкретного столбца, вы можете использовать метод queryForList
. В этом варианте использования я покажу на примере, что для результирующих элементов требуется SQL-запрос и тип возвращаемого значения. Речь идет о типе элементов, которые вы будете сохранять в список (List) Java. Например, если все элементы, которые вы извлекаете с помощью этого запроса, являются целыми числами, типом возврата будет Integer.class
. Основной пример использования метода выглядит так:
jdbcTemplate.queryForList(String sqlStatement, Class returnType);
Пример
Мы хотим сохранить в список Java все названия ингредиентов, которые есть в таблице ‘ingredient’. Мы также хотим вывести эти значения в консоль. Этого можно добиться следующим образом:
List<String> queryForColumn = jdbcTemplate.queryForList("select " +
"distinct name from ingredient", String.class);
System.out.println("All available ingredients = " + queryForColumn);
Поскольку все названия ингредиентов имеют тип String
, тип возвращаемого значения для метода queryForList
— String.class
. Вот что будет выведено на консоль:
All available ingredients = [chicken, red pepper, green pepper, yellow pepper, cheese, tomato]
queryForList — получение списка строк
Другой вариант использования метода queryForList
— получение сразу нескольких строк. В этом случае единственный параметр, требуемый при вызове этого метода, — это SQL-запрос, который собирает данные. Типом возврата будет список элементов типа map
, где каждая map
будет иметь ключ с типом String
и соответствующее значение с типом Object
. Этот метод выглядит так:
jdbcTemplate.queryForList(String sqlStatement);
Пример
Выберите все значения из таблицы ‘meal’, сохраните и выведите их в консоль.
List<Map<String, Object>> severalRowsAsListOfMaps = jdbcTemplate.queryForList("select * from meal;");
System.out.println("All available meals = " + severalRowsAsListOfMaps);
Вывод здесь представляет собой список элементов типа map
:
All available meals = [{meal_id=1, name=Chicken Fajita, category=lunch}, {meal_id=2, name=Enchilada, category=lunch}]
Передача параметров запросам
В некоторых случаях SQL-запросы нуждаются в передаче параметра для замены захардкоженного значения из запроса. Например, вы можете захотеть выполнить тот же запрос для поиска строки в базе данных на основе ее id
. Но вам может потребоваться передать id
в тест через DataProvider
. Следовательно, при каждом запуске метода для выполнения запроса у вас будет другое значение id
.
В этом случае для любого запроса, который вы хотите выполнить, вместо указания явного значения вы проставляете символ ?
. Это нужно сделать внутри SQL-запроса.
Пример
В тестовом методе нам нужно выяснить, сколько строк существует в таблице ingredient
с именем, которое предоставляется в виде параметра. Результат этого запроса будет сохранен в переменной типа int
и будет выведен в консоль. Этого можно добиться следующим образом:
Integer howManyUsages = jdbcTemplate.queryForObject("select count(*) "
+ "from ingredient where name=?", Integer.class, ingredientToLookFor);
System.out.println("How many time does the ingredient passed as "
+ "parameter appear in the DB " + " = " + howManyUsages);
Второй параметр, переданный методу queryForObject
, — это тип возвращаемого значения для запроса, а третий параметр — это имя параметра, который будет отправлен в запрос из DataProvider
. Например, если значение параметра ingredientToLookFor
будет ‘chicken’, вывод в консоль будет следующим:
How many time does the ingredient passed as parameter appear in the DB = 2
Извлечение данных в объект Java
Помните мою статью об использовании объектов Java для моделирования данных, извлеченных из БД? Вы можете легко использовать JdbcTemplate
для запроса базы данных и извлечения результата непосредственно в объект (Object). Все, что вам нужно для выполнения этой задачи, — это объект Java для моделирования данных; класс преобразователя строк (row mapper
), который сопоставляет столбец из базы данных со свойствами объекта; запрос, который извлекает данные в объект с помощью преобразователя строк.
Пример
Допустим, нам нужно смоделировать данные, соответствующие ингредиенту, название которого содержит текст ‘yellow’, в объект ингредиента (Ingredient Object
). Это означает, что мы хотим, чтобы объект имел те же свойства, что и ингредиент в таблице. Мы хотим сопоставить каждый столбец со свойством. Поэтому мы создадим объект Java под названием Ingredient. Его свойства будут следующими:
public int meal_id;
public String name;
public int quantity;
public String uom;
Рекомендуется синхронизировать имена свойств с именами столбцов базы данных. Таким образом, вы можете легко идентифицировать каждое свойство. Поскольку это объект, нам потребуется создать методы equals
, hashCode
и toString
. Пока я пропущу эту часть.
Вместо этого я покажу кое-что еще, что вам понадобиться, а именно сеттеры для каждого свойства. Вы можете легко автоматически сгенерировать их в IntelliJ, используя в редакторе сочетание клавиш Alt+Insert. Они будут выглядеть следующим образом:
public void setMeal_id ( int meal_id){
this.meal_id = meal_id;
}
public void setName (String name){
this.name = name;
}
public void setQuantity ( int quantity){
this.quantity = quantity;
}
public void setUom (String uom){
this.uom = uom;
}
Вы будете использовать их для отображения данных БД в свойства объекта. И это произойдет внутри класса преобразователя строк, который мы создадим следующим. Тело этого класса выглядит следующим образом:
public class IngredientRowMapper implements RowMapper<Ingredient> {
@Override
public Ingredient mapRow(ResultSet rs, int rowNum) throws SQLException {
Ingredient ingredient = new Ingredient();
ingredient.setMeal_id(rs.getInt("meal_id"));
ingredient.setName(rs.getString("name"));
ingredient.setQuantity(rs.getInt("quantity"));
ingredient.setUom(rs.getString("uom"));
return ingredient;
}
}
Как видите, этот класс должен реализовать интерфейс под названием RowMapper
. Из-за этого нам потребуется реализовать метод mapRow
. И внутри этого метода вы будете сопоставлять каждое свойство объекта со столбцом базы данных, используя сеттеры. Так, например, для свойства quantity
метод setQuantity
установит значение, извлеченное из строки, имя соответствующего столбца которой тоже quantity
.
Создав класс IntegerRowMapper
, мы можем выполнить поставленную задачу, используя queryForObject
для извлечения данных, соответствующих желтому (‘yellow’) ингредиенту:
Ingredient ingredient = jdbcTemplate.queryForObject("select * from "
+ "ingredient where name like '%yellow%'", new IngredientRowMapper());
System.out.println("The ingredient object = " + ingredient);
Результат этого запроса будет отображаться как объект с соответствующими свойствами:
The ingredient object = Ingredient{meal_id=1, name='yellow pepper', quantity=1, uom='piece'}
Заключение
Мы рассмотрели несколько способов работы с базами данных из наших автоматизированных тестов: нужно ли нам обновлять их или просто извлекать данные — в классе JdbcTemplate
есть множество методов, которые могут нам помочь с этим. Вы можете выбрать, какой из них использовать, в зависимости от того, что должен возвращать ваш SQL-запрос.
Узнать подробнее о курсе "Java QA Automation Engineer".
Смотреть открытый вебинар на тему «HTTP. Postman, Newman, Fiddler (Charles), curl, SOAP. SoapUI».
sshikov
Вообще-то для операций типа DDL есть execute, который в отличие от update, не возвращает число обновленных строк (и не ждет от DDL такого числа).
Ну а «queryForList — получить столбец» это просто неверно. queryForList возващает список (в частном случае — например из Map). Т.е. это не столбец, а набор строк, выбранных запросом, и возможно обработанных mapper-ом. А уж столбец там будет, или все столбцы — это смотря какой запрос, и какой маппер.
И вообще автор опускает кучу важных деталей. Например, если вы выполните queryForObject, но при этом запрос ваш вернет две строки — вы получите exception, что ожидался только один ответ. Это позволяет в некоторой степени дополнительно контролировать правильность запросов.