В этом уроке рассмотрим, как тестировать Room. Напишем несколько тестов для Dao и протестируем миграцию.
Полный список уроков курса:
- Урок 1. Lifecycle
- Урок 2. LiveData
- Урок 3. LiveData. Дополнительные возможности
- Урок 4. ViewModel
- Урок 5. Room. Основы
- Урок 6. Room. Entity
- Урок 7. Room. Insert, Update, Delete, Transaction
- Урок 8. Room. Query
- Урок 9. Room. RxJava
- Урок 10. Room. Запрос из нескольких таблиц. Relation
- Урок 11. Room. Type converter
- Урок 12. Room. Миграция версий базы данных
- Урок 13. Room. Тестирование
- Урок 14. Paging Library. Основы
- Урок 15. Paging Library. PagedList и DataSource. Placeholders.
- Урок 16. Paging Library. LivePagedListBuilder. BoundaryCallback.
- Урок 17. Paging Library. Виды DataSource
- Урок 18. Android Data Binding. Основы
- Урок 19. Android Data Binding. Код в layout. Доступ к View
- Урок 20. Android Data Binding. Обработка событий
- Урок 21. Android Data Binding. Observable поля. Двусторонний биндинг.
- Урок 22. Android Data Binding. Adapter. Conversion.
- Урок 23. Android Data Binding. Использование с include, ViewStub и RecyclerView.
- Урок 24. Navigation Architecture Component. Введение
- Урок 25. Navigation. Передача данных. Type-safe аргументы.
- Урок 26. Navigation. Параметры навигации
- Урок 27. Navigation. NavigationUI.
- Урок 28. Navigation. Вложенный граф. Global Action. Deep Link.
- Урок 29. WorkManager. Введение
- Урок 30. WorkManager. Критерии запуска задачи.
- Урок 31. WorkManager. Последовательность выполнения задач.
- Урок 32. WorkManager. Передача и получение данных
- Урок 33. Практика. О чем это будет.
- Урок 34. Практика. TodoApp. Список задач.
- Урок 35. Практика. TodoApp. Просмотр задачи
Dao
В Dao вы прописываете различные операции с Entity объектами: чтение, вставка, изменение, удаление.
Пример Dao:
@Dao
public interface EmployeeDao {
@Query("SELECT * FROM employee")
List<Employee> getAll();
@Query("SELECT * FROM employee ORDER BY salary DESC")
List<Employee> getAllOrderBySalary();
@Insert
void insert(Employee employee);
@Insert
void insertAll(List<Employee> employees);
@Update
int update(Employee employee);
@Delete
void delete(Employee employee);
@Query("DELETE FROM employee")
void deleteAll();
}
Для этих методов можно написать несколько тестов. Подробнее о том, как создавать тесты, как работают assert методы и пр., вы можете прочитать в курсе Тестирование.
Я же сразу покажу содержимое тестового класса.
@RunWith(AndroidJUnit4.class)
public class EmployeeDaoTest {
private AppDatabase db;
private EmployeeDao employeeDao;
@Before
public void createDb() throws Exception {
db = Room.inMemoryDatabaseBuilder(
InstrumentationRegistry.getContext(),
AppDatabase.class)
.build();
employeeDao = db.employeeDao();
}
@After
public void closeDb() throws Exception {
db.close();
}
}
Обратите внимание, что тест инструментальный. Т.е. его надо будет запускать на устройстве или эмуляторе.
В переменной db будет хранится база. При ее создании мы использовали метод inMemoryDatabaseBuilder. В результате, при запуске теста данные базы будут находится в памяти и после завершения теста буду удалены.
В Before методе мы создаем базу и Dao, а в After методе - закрываем базу.
Рассмотрим несколько возможных тестовых методов
Вставляем одну запись и проверяем, что она же считалась.
@Test
public void whenInsertEmployeeThenReadTheSameOne() throws Exception {
List<Employee> employees = EmployeeTestHelper.createListOfEmployee(1);
employeeDao.insert(employees.get(0));
List<Employee> dbEmployees = employeeDao.getAll();
assertEquals(1, dbEmployees.size());
assertTrue(EmployeeTestHelper.employeesAreIdentical(employees.get(0), dbEmployees.get(0)));
}
В помощь себе я создал класс EmployeeTestHelper, который имеет пару полезных методов:
- createListOfEmployee создает список с указанным количеством Employee объектов, заполненных рандомными данными
- employeesAreIdentical проверяет, что все два указанных Employee объекта равны по всем полям
Следующий тест проверит, что при вызове метода update запись должна обновится в базе.
@Test
public void whenUpdateEmployeeThenReadTheSameOne() throws Exception {
List<Employee> employees = EmployeeTestHelper.createListOfEmployee(1);
Employee employee = employees.get(0);
employeeDao.insert(employee);
employee.salary += 100;
employee.name += " test";
employeeDao.update(employee);
List<Employee> dbEmployees = employeeDao.getAll();
assertTrue(EmployeeTestHelper.employeesAreIdentical(employees.get(0), dbEmployees.get(0)));
}
При вставке нескольких записей, все они должны оказаться в базе
@Test
public void whenInsertEmployeesThenReadThem() throws Exception {
List<Employee> employees = EmployeeTestHelper.createListOfEmployee(5);
employeeDao.insertAll(employees);
assertEquals(5, employeeDao.getAll().size());
}
Метод deleteAll очищает всю базу.
@Test
public void whenDeleteAllThenReadNothing() throws Exception {
List<Employee> employees = EmployeeTestHelper.createListOfEmployee(5);
employeeDao.insertAll(employees);
employeeDao.deleteAll();
assertTrue(employeeDao.getAll().isEmpty());
}
Метод getAllOrderBySalary должен возвращать данные отсортированные по зарплате
@Test
public void checkOrderBySalary() throws Exception {
List<Employee> employees = EmployeeTestHelper.createListOfEmployee(5);
employeeDao.insertAll(employees);
Collections.sort(employees, new Comparator<Employee>() {
@Override
public int compare(Employee o1, Employee o2) {
return o2.salary - o1.salary;
}
});
assertEquals(employees, employeeDao.getAllOrderBySalary());
}
Чтобы последний метод работал корректно, необходимо добавить реализацию методов equals и hashcode для Employee
@Entity()
public class Employee {
@PrimaryKey
public long id;
public String name;
public int salary;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Employee employee = (Employee) o;
return id == employee.id;
}
@Override
public int hashCode() {
return (int) (id ^ (id >>> 32));
}
}
Миграция
Рассмотрим тестирование миграции на простом примере. У нас есть база версии 1 и Entity класс.
@Entity()
public class Employee {
@PrimaryKey
public long id;
public String name;
public int salary;
}
Мы добавим новое поле в этот класс, настроим миграцию и создадим тест миграции.
Сначала необходимо настроить экспорт схемы вашей базы в json файлы. Это делается в build.gradle файле модуля:
android {
...
defaultConfig {
...
javaCompileOptions {
annotationProcessorOptions {
arguments = ["room.schemaLocation": "$projectDir/schemas".toString()]
}
}
}
После компиляции приложения, в папке проекта появится папка schemas/<application_package>, в которой будут хранится схемы вашей базы данных. Текущая версия базы = 1. Для нее будет создан файл 1.json.
Cодержимое этого файла представляет собой текущую схему базы:
{
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "f644b5f11fc9422f1830daaaf37a190c",
"entities": [
{
"tableName": "Employee",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `salary` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "salary",
"columnName": "salary",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
}
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"f644b5f11fc9422f1830daaaf37a190c\")"
]
}
}
Давайте добавим новое поле в Employee. Это поле будет содержать налоговый класс сотрудника. Класс может принимать значение 1,2 и 3 в зависимости от размера зарплаты. Будем считать, что у нас прогрессивная шкала налогообложения )
Меняем версию базы в AppDatabase на 2. И в класс Employee добавляем поле taxclass:
@Entity()
public class Employee {
@PrimaryKey
public long id;
public String name;
public int salary;
public int taxclass;
}
Компилим проект, и в папке schemas появляется файл 2.json. Число 2 означает, что в файл описывает схему базы версии 2. Т.е. в ней теперь будет информация о поле taxclass.
В итоге, в папке schemas у нас формируется что-то типа журнала версий базы данных. Зачем это нужно, станет понятно чуть позже.
Настраиваем миграцию. Подробно об этом я рассказывал в прошлом уроке. Здесь укажу лишь, как будет выглядеть Migration с первой на вторую версию:
public static final Migration MIGRATION_1_2 = new Migration(1, 2) {
@Override
public void migrate(final SupportSQLiteDatabase database) {
database.execSQL("ALTER TABLE employee ADD COLUMN taxclass INTEGER DEFAULT 0 NOT NULL");
database.execSQL("UPDATE employee SET taxclass = 1 WHERE salary < 10000");
database.execSQL("UPDATE employee SET taxclass = 2 WHERE salary BETWEEN 10000 AND 30000");
database.execSQL("UPDATE employee SET taxclass = 3 WHERE salary > 30000");
}
};
Здесь мы добавляем новое поле в таблицу и настраиваем классы. Если зарплата меньше 10000, то класс = 1. Если от 10000 до 30000, то 2. Если выше 30000, то 3.
Миграция готова. При запуске приложения Room выполнит переход на вторую версию базы. А мы со своей стороны можем написать тест, который смоделирует этот переход. Т.е. тест создаст базу версии 1, заполнит ее данными, выполнит миграцию на версию 2 и проверит, что все прошло успешно.
Создаем тест. В секцию dependencies добавьте:
androidTestImplementation "android.arch.persistence.room:testing:1.0.0"
Это даст нам доступ к инструменту тестирования MigrationTestHelper.
А в секцию android добавьте следующий sourceSets:
android {
...
sourceSets {
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
}
}
Это даст тесту доступ к папке Schemas, чтобы он смог считать схемы базы.
Тестовый класс:
@RunWith(AndroidJUnit4.class)
public class MigrationTest {
private static final String TEST_DB = "migration-test";
@Rule
public MigrationTestHelper helper;
public MigrationTest() {
helper = new MigrationTestHelper(InstrumentationRegistry.getInstrumentation(),
AppDatabase.class.getCanonicalName(),
new FrameworkSQLiteOpenHelperFactory());
}
@Test
public void migrate1To2() throws IOException {
SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, 1);
db.execSQL("INSERT INTO employee VALUES (1, 'name 1', 5000)");
db.execSQL("INSERT INTO employee VALUES (2, 'name 2', 10000)");
db.execSQL("INSERT INTO employee VALUES (3, 'name 3', 20000)");
db.execSQL("INSERT INTO employee VALUES (4, 'name 4', 30000)");
db.execSQL("INSERT INTO employee VALUES (5, 'name 5', 35000)");
db.close();
db = helper.runMigrationsAndValidate(TEST_DB, 2, true, MIGRATION_1_2);
Cursor cursor = db.query("select * from employee");
assertEquals(5, cursor.getCount());
while (cursor.moveToNext()) {
int salary = cursor.getInt(cursor.getColumnIndex("salary"));
int taxClass = cursor.getInt(cursor.getColumnIndex("taxclass"));
int expectedTaxClass = 0;
if (salary < 10000) {
expectedTaxClass = 1;
} else if (salary <= 30000) {
expectedTaxClass = 2;
} else {
expectedTaxClass = 3;
}
assertEquals("Wrong taxclass for salary: " + salary, expectedTaxClass, taxClass);
}
}
}
В конструкторе создаем MigrationTestHelper. Он также будет использован в качестве Rule.
Разбираем метод migrate1To2.
Сначала мы методом createDatabase создаем базу первой версии. Это возможно благодаря тому, что в папке schemas есть файл 1.json и MigrationTestHelper по нему может создать базу.
Далее заполняем базу тестовыми данными и закрываем ее. Закрывать необходимо, т.к. сейчас структура базы будет меняться.
Метод runMigrationsAndValidate выполнит миграцию базы на вторую версию (выполнив код из MIGRATION_1_2) и проверит, что получившаяся структура базы соответствует схеме из файла 2.json.
Далее мы из новой полученной базы читаем данные по сотрудникам и проверяем, что MIGRATION_1_2 отработал корректно и проставил работникам правильные налоговые классы. Для каждого сотрудника мы сами по зарплате вычисляем налоговый класс и сверяем его с тем, который пришел из базы.
Таким образом тест выполнил миграцию базы и проверил, что структура и данные были преобразованы корректно.
Схемы
Одно небольшое, но важное замечание про схемы в папке schemas. Они генерируются при компиляции проекта, и тут надо быть внимательными, т.к. может получиться следующая ситуация:
- есть база версии 1 и, соответственно, файл 1.json
- решаем поменять структуру базы
- добавляем новое поле в Entity класс, но забываем поднять версию базы
- компилируем проект и получаем в 1.json уже новую структуру базы
- настоящая схема версии 1 теперь утеряна
После этого миграционный тест не сможет создать базу первой версии, потому что 1.json описывает уже вторую версию.
Чтобы избежать этого, сначала всегда поднимайте версию приложения в AppDatabase классе, а потом уже меняйте структуру Entity классов.
Присоединяйтесь к нам в Telegram:
- в канале StartAndroid публикуются ссылки на новые статьи с сайта startandroid.ru и интересные материалы с хабра, medium.com и т.п.
- в чатах решаем возникающие вопросы и проблемы по различным темам: Android, Compose, Kotlin, RxJava, Dagger, Тестирование, Performance
- ну и если просто хочется поговорить с коллегами по разработке, то есть чат Флудильня

