В этом уроке рассмотрим, как тестировать 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
- ну и если просто хочется поговорить с коллегами по разработке, то есть чат Флудильня