Room. Тестування
У цьому уроці розглянемо, як тестувати Room. Напишемо кілька тестів для Dao і протестуємо міграцію.
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();
}
Для цих методів можна написати кілька тестів.
Я ж одразу покажу вміст тестового класу.
@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.
Вміст цього файлу являє собою поточну схему бази:
{
"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 класів.