wpf 自动化测试,缓存
Imagine that you use a framework like requery that generates the tables in your database based on the current state of your @Entity
objects. You ship the version A of your app, and everything goes smoothly.
我 MAGINE你使用像一个框架, 重新查询 ,基于您的当前状态,在数据库中生成的表@Entity
对象。 您发布了应用程序的版本A,一切顺利。
Sooner or later, you decide that you need a change in the structure of the database, be it a new column, foreign key, table, or pretty much any change you can think of. This change will be shipped in the latest version of your app; let’s call it version B.
迟早,您决定需要更改数据库的结构,无论是新列,外键,表还是几乎可以想到的任何更改。 此更改将以您应用的最新版本提供; 我们将其称为版本B。
Requery guarantees that it’ll generate the tables correctly for the new users who do a clean install of version B. What do you do with the existing ones, who have version A and now upgrade to version B?
重新查询保证它会为进行全新安装版本B的新用户正确生成表。您如何处理现有用户(具有版本A,现在又升级到版本B)?
You write the migration procedures, and you put them in the callback that gets invoked when the database gets upgraded from the old version to the newest one (in case of requery, that would be DatabaseSource#onUpgrade
).
编写迁移过程,然后将它们放入数据库从旧版本升级到最新版本时调用的回调中(如果重新查询, DatabaseSource#onUpgrade
)。
Later on, you ship the version C that has some additional migration procedures. And so on, until you stumble upon a question (and you better stumble upon it while theorizing about your code, not when you’re reviewing the crashes from your crash reporting tool).
稍后,您将发布具有一些其他迁移过程的版本C。 依此类推,直到您偶然发现了一个问题(最好是在理论化代码时就偶然发现了这个问题,而不是在您从崩溃报告工具中查看崩溃时)。
How do I ensure that my old users have exactly the same database schema as the new ones? How do I ensure that none of my migration procedures were lost, and the result of their work leads to exactly the same schema as the one created on the devices of my latest users?
如何确保我的旧用户具有与新用户完全相同的数据库架构 ? 如何确保所有迁移过程都不会丢失,其工作结果所导致的架构与最新用户在设备上创建的架构完全相同?
sqlite_master (sqlite_master)
The obvious way to cope with such situations in the future is to add a safety net via automated tests. Unfortunately, requery does not have any support for migration tests, similar to what either Room or SQLDelight have. But, SQLite itself has one attractive feature which we can use to write our own simple solution.
未来应对这种情况的明显方法是通过自动测试添加安全网。 不幸的是,与Room或SQLDelight相似,重新查询不支持任何迁移测试。 但是,SQLite本身具有一项吸引人的功能,我们可以使用它来编写自己的简单解决方案。
Meet the SQLITE_MASTER table. It defines the database’s schema, including the schema of its tables, views, triggers, and indices.
符合SQLITE_MASTER表。 它定义数据库的架构,包括其表,视图,触发器和索引的架构。
Let’s look at an example of how one can use this table to inspect the database schema. We’ll start with an empty database and add a single table to it, named User
:
让我们看一个示例,说明如何使用此表检查数据库模式。 我们将从一个空的数据库开始,并向其中添加一个名为User
表:
sqlite> create table User (id integer primary key not null, name text, surname text);
Let’s look at what’s inside of sqlite_master
table:
让我们看一下sqlite_master
表的内容:
sqlite> .headers on
sqlite> select * from sqlite_master;
type|name|tbl_name|rootpage|sql
table|User|User|3|create table User (id integer primary key not null, name text, surname text)
(I started with .headers on
to make sure SQLite includes the columns’ names in its output). Here we have five different fields, and what we’re mostly interested in are the ones named name
and sql
— these two define the table’s name and the query, which can be used to create this table.
(我开始使用.headers on
来确保SQLite在其输出中包括列的名称)。 在这里,我们有五个不同的字段,而我们最感兴趣的是名为name
和sql
的字段-这两个字段定义了表的名称和查询,可用于创建该表。
Now, what’s interesting about this table is that its content changes whenever you update the database schema. Consider the case when we want to add the date of birth as an extra column to the user:
现在,关于此表的有趣之处在于,只要您更新数据库架构,它的内容就会更改。 考虑以下情况,当我们要将出生日期作为额外的列添加到用户时:
sqlite> alter table User add column birth_date integer;
sqlite> select name, sql from sqlite_master;
name|sql
User|create table User (id integer primary key not null, name text, surname text, birth_date integer);
So, this gives us one piece of a puzzle.
因此,这给了我们一个难题。
该方法 (The approach)
The migration procedures are usually applied one after another, without doing some neat tricks like:
迁移过程通常一个接一个地应用,而没有做一些巧妙的技巧,例如:
- In the first version of the database, table A had a column “a.” 在数据库的第一个版本中,表A的列为“ a”。
- In the second version, we added the column “b” to the same table. 在第二个版本中,我们在同一表中添加了“ b”列。
- In the third version, we dropped the table A altogether, so… 在第三个版本中,我们完全删除了表A,因此…
- If the user updates from v1 to v3, we can just drop table “A” and be done with it without creating column “b” only to be removed afterward. 如果用户从v1升级到v3,我们只需删除表“ A”即可完成操作,而无需创建列“ b”,以后再将其删除。
Trying to be fancy is the root of evil.
幻想是邪恶的根源。
So, it leaves us with the following:
因此,它为我们提供了以下内容:
- Create the database, let requery create all required tables and foreign keys. 创建数据库,让重新查询创建所有必需的表和外键。
- Collect the information on the schema. 收集有关架构的信息。
- Create the database having the very first schema shipped to the users, apply all migrations on top of it, reusing the same migration code that we use in production. 创建具有交付给用户的第一个架构的数据库,在其之上应用所有迁移,并重用我们在生产中使用的相同迁移代码。
- Collect the information on the schema. 收集有关架构的信息。
- Assert that information we got on stages 2 and 4 is precisely the same. 断言我们在第2阶段和第4阶段获得的信息是完全相同的。
第2、4和5步(详细信息请参见“魔鬼”) (Steps 2, 4 and 5 (the devil’s in the details))
Now, how would we extract the database schemas, and how would we assert that they match each other? The naive approach would be to compare the table creation queries obtained for the new users and the queries received after the migration procedures as strings, but that would quickly prove to be inadequate:
现在,我们将如何提取数据库模式,以及如何断言它们彼此匹配? 天真的方法是将为新用户获得的表创建查询与在迁移过程之后收到的查询作为字符串进行比较,但这很快就会被证明是不够的:
assertEquals("create table User (id integer primary key not null)", "create table User (id integer not null primary key)") // woopsassertEquals("CREATE TABLE User (id INTEGER PRIMARY KEY NOT NULL)", "create table User (id integer primary key not null)") // woops #2
“But that’s simple! I can make case-insensitive comparison and reorder all the tokens in the column definitions so that their order becomes fixed”.
“但这很简单! 我可以进行不区分大小写的比较,并对列定义中的所有标记重新排序,以使它们的顺序固定。”
Believe me, you don’t want to take that route. There are just too many ifs here. What if you make the case-insensitive comparison when you should’ve done case-sensitive instead? What if your reordering logic does not cover some nitty-gritty scenario?
相信我,你不想走那条路。 这里有太多的假设。 如果在进行区分大小写的比较时进行不区分大小写的比较怎么办? 如果您的重新排序逻辑未涵盖某些实际情况,该怎么办?
The proper way to handle that task would be to use the tool like ANTLR to parse the queries and compare them based on whether they’re the same or not in the sense of programming language grammar.
处理该任务的正确方法是使用ANTLR之类的工具来解析查询,并根据编程语言语法的含义是否相同来对查询进行比较。
I don’t want to deviate too much on explaining what exactly ANTLR is and how you can use it in your projects (if you’re interested to learn more, I can personally recommend this tutorial). Still, here I just want to say that ANTLR is a tool that allows you to generate the programming language parser based on the grammar definition file of that particular language. Once we have that parser, we can traverse the programs written in this language as parse trees and infer various info based on this traversal, e.g., whether two queries are the same or not.
我不想在解释ANTLR的确切含义以及如何在项目中使用它方面有太多的偏离(如果您想了解更多信息,我可以个人推荐本教程 )。 尽管如此,在这里我只想说说ANTLR是一种工具,它使您可以基于特定语言的语法定义文件来生成编程语言解析器。 一旦有了该解析器,我们就可以遍历用这种语言编写的程序作为解析树,并基于该遍历来推断各种信息,例如,两个查询是否相同。
The net result of that research is a tiny library called Migrations (yup, I’m not good with naming things, I know). It is still far from being perfect as it only supports columns and foreign keys, but pull requests are always welcomed. :)
这项研究的最终结果是一个名为Migrations的小型图书馆(是的,我不擅长命名)。 由于它仅支持列和外键,因此还远远不够完美,但始终欢迎请求请求。 :)
一个例子 (An example)
The full example of how it can be used in your Espresso tests is accessible in the sample
module (check DatabaseMigrationTest). I’ve added it to gist file, so let’s go through it line by line to see how it works:
在sample
模块中可以找到有关如何在Espresso测试中使用它的完整示例(请参阅DatabaseMigrationTest )。 我已将其添加到gist文件中,因此让我们逐行查看它的工作原理:
package aga.android.migrations.sample
import aga.android.migrations.AndroidDatabaseSchema
import aga.android.migrations.sample.cache.Database.Companion.DB_VERSION
import aga.android.migrations.sample.cache.Database.Companion.build
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import io.requery.android.sqlite.DatabaseSource
import io.requery.sql.SchemaModifier
import io.requery.sql.TableCreationMode
import junit.framework.TestCase.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
import java.io.File
@RunWith(AndroidJUnit4::class)
class DatabaseMigrationTest {
private val schemaExtractor = AndroidDatabaseSchema()
@Test
fun testDbMigration() {
// given
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
val testContext = InstrumentationRegistry.getInstrumentation().context
val database: DatabaseSource = build(appContext)
SchemaModifier(
database.configuration
).createTables(TableCreationMode.DROP_CREATE)
// when
val schemaForNewUsers = schemaExtractor.extract(database.readableDatabase)
schemaExtractor.cleanup(database.writableDatabase)
recreateTheInitialDbState(testContext, database.writableDatabase)
database.onUpgrade(database.writableDatabase, 1, DB_VERSION)
val schemaForMigratedUsers = schemaExtractor.extract(database.readableDatabase)
// then
assertEquals(schemaForNewUsers, schemaForMigratedUsers)
}
private fun recreateTheInitialDbState(context: Context, database: SQLiteDatabase) {
val initialDbStatePath = "initial-db-state"
context
.assets
?.list(initialDbStatePath)
?.map {
context.resources.assets.open("$initialDbStatePath/$it").bufferedReader()
}
?.forEach { sqlStatementReader ->
sqlStatementReader.use {
database.execSQL(it.readText())
}
}
}
}
You start by creating the AndroidDatabaseSchema
instance (line 20). That’s the class that can do two things: it can extract the schema info based on the instance of SQLiteDatabase, and it can drop all tables from the database to prepare it for the next round of testing.
首先创建AndroidDatabaseSchema
实例(第20行)。 该类可以做两件事:它可以基于SQLiteDatabase的实例提取架构信息,并且可以从数据库中删除所有表,以准备进行下一轮测试。
The database
(line 28) is an instance of the Database
class — that’s requery’s own wrapper around SQLiteDatabase. We will use it later to extract the schema information from it.
database
(第28行)是Database
类的实例-它是SQLiteDatabase的重新查询自己的包装器。 稍后我们将使用它从中提取模式信息。
Line 30 is where we ask requery to create the database based on the current state of all your @Entity
classes/interfaces. It is the state of the database that all your new users will get when they do a clean install of the latest version of your app. Hence, we assume that this database can give us the correct schema, i.e., the schema that we expect to have after all our migration procedures are done.
第30行是我们要求重新查询以基于所有@Entity
类/接口的当前状态创建数据库的位置。 这是您的所有新用户在全新安装应用程序的最新版本时将获得的数据库状态。 因此,我们假定该数据库可以为我们提供正确的架构,即在完成所有迁移过程之后我们期望的架构。
Line 35 is where we extract the database’s schema. Once we obtained it, we drop all the tables from the database (line 37), recreate the state of the database that we had in the very first version of our app (line 39) and run the migration procedures by invoking precisely the same method that will be invoked by requery when it migrates your database (line 41). That part is crucial — you want to be 100% sure that your test runs the same migrations and in the same manner as your app.
第35行是我们提取数据库模式的位置。 一旦获得它,就从数据库中删除所有表(第37行),重新创建应用程序第一个版本中的数据库状态(第39行),并通过调用完全相同的方法来运行迁移过程迁移数据库时,requery将调用该方法 (第41行)。 这部分至关重要-您要100%确保测试运行与应用程序相同的迁移,并以相同的方式运行。
Once we’re done with that, we extract the migrated schema (line 43) and compare it to the schema that we obtained at the beginning of our test.
完成之后,我们将提取迁移的模式(第43行),并将其与在测试开始时获得的模式进行比较。
I highly recommend adding such a test to your test suite and running it on Bitrise / Github Actions or whatever you use for CI/CD purposes, at least for every RC build. That way, you’ll have more peace of mind, knowing that if you forgot to add the migration procedures, the build will fail and point you to that. Or, if your migration procedures leave the database in a slightly different state compared to the fresh install, that will be caught too.
我强烈建议将这样的测试添加到您的测试套件中,并至少在每个RC构建中,在Bitrise / Github Actions或用于CI / CD的任何工具上运行它。 这样,您将更加放心,知道如果您忘记添加迁移过程,构建将失败并指向您。 或者,如果您的迁移过程使数据库处于与全新安装相比稍有不同的状态,那么也会被捕获。
The only thing that I wanted to mention at the end of that article is that the database migrations are nasty things that, when done incorrectly, can do a lot of harm to your app’s ratings. Hence, it’s crucial that you deploy some automated tests around it, and do it as fast as possible.
我想在文章末尾提到的唯一一件事是,数据库迁移是令人讨厌的事情,如果操作不当,可能会对应用程序的评级造成很多损害。 因此,至关重要的是您必须围绕它部署一些自动化测试,并尽快进行。
If you can pick the SQLite wrapper for your project and you prefer Room or SQLDelight, then, by all means, go for it and test your migrations via them. But, you’re on a legacy project, and you can’t or don’t have time to switch to either of these solutions, you can always cook something up based on SQLITE_MASTER
and ANTLR
.
如果您可以为项目选择SQLite包装器,并且更喜欢Room或SQLDelight,那么请务必尝试并通过它们测试迁移。 但是,您在一个旧项目上,您无法或没有时间切换到这两个解决方案中的任何一个,您始终可以根据SQLITE_MASTER
和ANTLR
来做一些事情。
Or you can give migrations a try.
或者,您可以尝试迁移 。
(Was it a pun? Was it intended? I don’t know!)
(是双关语吗?是故意的吗?我不知道!)
翻译自: https://proandroiddev.com/automated-tests-for-cache-migrations-on-android-ea2d88197f3e
wpf 自动化测试,缓存