В этом уроке рассмотрим, как тестировать 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();
}

Для этих методов можно написать несколько тестов. Подробнее о том, как создавать тесты, как работают 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 

- ну и если просто хочется поговорить с коллегами по разработке, то есть чат Флудильня




Language

Автор сайта

Дмитрий Виноградов

Подробнее можно посмотреть или почитать.

Никакие другие люди не имеют к этому сайту никакого отношения и просто занимаются плагиатом.

Социальные сети

 

В канале я публикую ссылки на интересные и полезные статьи по Android

В чате можно обсудить вопросы и проблемы, возникающие при разработке



Группа ВКонтакте



Поддержка проекта

Яндекс
410011180491924

WebMoney
R248743991365
Z551306702056

Paypal