20、数据库单元测试:JUnit与SQLite测试、Room测试框架、内存数据库(in-memory)、测试数据准备与清理、Mockito模拟依赖
说实话,很多开发兄弟写SQLite代码时,都是跑一遍App,点几个按钮,看数据对不对。嗯,这种手动验证方式,我早年也干过。直到有一次,一个数据库迁移的bug在生产环境炸了,用户数据全丢了……从那以后,我再也不敢不写单元测试了。
数据库测试,说白了就是给你的数据层上保险。你想想看,CRUD操作、表结构变更、数据迁移,这些环节一旦出问题,后果很严重。今天我就把这几年的实战经验掰开揉碎,跟你聊聊怎么把数据库单元测试写好。
JUnit与SQLite测试:最基础的组合
JUnit是Android测试的基石。配合SQLite,我们可以直接测试数据库的增删改查。我个人习惯用JUnit 4,稳定、生态好。
@RunWith(AndroidJUnit4::class)
class DatabaseTest {
private lateinit var db: SQLiteDatabase
@Before
fun setUp() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
db = SQLiteDatabase.create(null) // 创建内存数据库
db.execSQL("CREATE TABLE user (id INTEGER PRIMARY KEY, name TEXT)")
}
@Test
fun testInsertAndQuery() {
db.execSQL("INSERT INTO user (id, name) VALUES (1, '张三')")
val cursor = db.rawQuery("SELECT * FROM user WHERE id = ?", arrayOf("1"))
assertTrue(cursor.moveToFirst())
assertEquals("张三", cursor.getString(cursor.getColumnIndex("name")))
cursor.close()
}
@After
fun tearDown() {
db.close()
}
}
SQLiteDatabase.create(null) 创建内存数据库,比用文件数据库快10倍以上。我在一个日活百万的项目里,把测试从文件数据库切到内存数据库后,CI构建时间从40分钟降到了12分钟。
Room测试框架:官方推荐的姿势
Room是Google的亲儿子,测试起来比原生SQLite方便不少。你只需要用 Room.inMemoryDatabaseBuilder 就能快速创建测试数据库。
@RunWith(AndroidJUnit4::class)
class UserDaoTest {
private lateinit var db: AppDatabase
private lateinit var userDao: UserDao
@Before
fun createDb() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
db = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java)
.allowMainThreadQueries() // 测试中允许主线程操作
.build()
userDao = db.userDao()
}
@Test
fun testInsertAndGetUser() {
val user = User(1, "李四", 25)
userDao.insert(user)
val loaded = userDao.getUser(1)
assertEquals("李四", loaded.name)
}
@After
fun closeDb() {
db.close()
}
}
allowMainThreadQueries() 只应该在测试中使用。生产环境如果这么干,ANR会教你做人。我曾经在一个新项目里忘了去掉这个配置,结果线上用户反馈App卡顿,查了半天才发现是测试代码漏改了。
内存数据库(in-memory):速度与隔离的利器
内存数据库,说白了就是把数据库建在RAM里。它的好处很明显:
- 速度快:没有磁盘I/O,测试执行时间缩短80%以上
- 自动清理:测试结束,数据库自动销毁,不留痕迹
- 隔离性好:每个测试用例都有自己的数据库,互不干扰
为什么会推荐用内存数据库?你想想看,如果每个测试都读写同一个文件数据库,测试顺序稍微一变,结果就全乱了。我见过一个团队,他们的测试经常随机失败,查了三天才发现是测试数据互相污染了。
// 创建内存数据库的两种方式
// 方式一:原生SQLite
val db = SQLiteDatabase.create(null)
// 方式二:Room
val db = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java)
.build()
测试数据准备与清理:别让数据污染你的测试
数据准备和清理,是数据库测试里最容易翻车的地方。我的原则是:每个测试用例都是独立的王国。
数据准备策略
| 策略 | 适用场景 | 我的建议 |
|---|---|---|
| @Before 中插入 | 少量基础数据 | 最常用,简单直接 |
| 测试方法内插入 | 特定测试数据 | 数据量大时推荐 |
| 外部JSON/CSV文件 | 大量测试数据 | 适合回归测试 |
class UserDaoTest {
@Before
fun setUp() {
// 准备基础数据
userDao.insert(User(1, "基础用户", 20))
userDao.insert(User(2, "测试用户", 30))
}
@Test
fun testQueryByName() {
val result = userDao.queryByName("测试用户")
assertNotNull(result)
assertEquals(30, result.age)
}
@After
fun cleanUp() {
// 清理所有数据
userDao.deleteAll()
}
}
Mockito模拟依赖:隔离外部干扰
数据库测试里,有时候你不想真的操作数据库,只想验证某个方法有没有被正确调用。这时候Mockito就派上用场了。
@RunWith(MockitoJUnitRunner::class)
class UserRepositoryTest {
@Mock
private lateinit var userDao: UserDao
@InjectMocks
private lateinit var repository: UserRepository
@Test
fun testSaveUser_callsDaoInsert() {
val user = User(1, "王五", 28)
repository.saveUser(user)
verify(userDao).insert(user)
}
@Test
fun testGetUser_returnsFromDao() {
val expected = User(1, "赵六", 22)
`when`(userDao.getUser(1)).thenReturn(expected)
val actual = repository.getUser(1)
assertEquals(expected, actual)
}
}
知识体系总览
下面这张图,是我整理的数据层测试知识体系。你可以把它当作一个检查清单,看看自己还有哪些没覆盖到。
数据库单元测试,说白了就是给你的数据层上双保险。我见过太多项目,数据库代码写得很漂亮,但一上线就出问题。原因很简单——没测试。你想想看,一个没有测试的数据库操作,就像在悬崖边走路,你不知道哪一步会踩空。
从JUnit+SQLite的基础组合,到Room的官方框架,再到内存数据库的提速技巧,最后用Mockito隔离外部依赖——这套组合拳打下来,你的数据库代码基本就稳了。嗯,剩下的就是坚持写、坚持跑。
公众号:蓝海资料掘金营,微信deep3321