引言
当各路高手还在为“该禁用不稳定的测试代码还是重试它们”争论不休时,我想直接给你们展示如何实现后者。
此方法专供勇士——后果自负哦!
实现重试策略
我们先创建两个类:
- Retry - 用于重试任意特定测试代码
- RetryRule - 负责整个测试套件的重试逻辑
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement
/** Annotation to retry a specific failed test. **/
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION)
public annotation class Retry(val count: Int)
/** Rule to retry all failed tests. **/
public class RetryRule(private val count: Int) : TestRule {
override fun apply(base: Statement, description: Description): Statement = statement(base, description)
private fun statement(base: Statement, description: Description): Statement {
return object : Statement() {
@Throws(Throwable::class)
override fun evaluate() {
val retryAnnotation: Retry? = description.getAnnotation(Retry::class.java)
val retryCount = retryAnnotation?.count ?: count
var caughtThrowable: Throwable? = null
for (i in 0 until retryCount) {
try {
System.err.println("${description.displayName}: run #${(i + 1)} started.")
base.evaluate()
return
} catch (t: Throwable) {
System.err.println("${description.displayName}: run #${(i + 1)} failed.")
caughtThrowable = t
}
}
throw caughtThrowable ?: IllegalStateException()
}
}
}
}
接下来,我们创建两个简单的测试用例,它们将绝望地尝试断言两个不同的数字不相等:
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
abstract class BaseTestClass {
@get:Rule
val retryRule = RetryRule(count = 3)
}
class SampleTest : BaseTestClass() {
@Test // <~~~~~~ This test have 3 attempts to pass
fun testNewFancyRetryRule() {
assertEquals(33, 42)
}
@Retry(count = 4)
@Test // <~~~~~~ This test have 4 attempts to pass
fun testNewFancyRetryAnnotation() {
assertEquals(42, 33)
}
}
重试之前清理数据库
很可能,你希望每次重试时环境尽可能干净。
为实现这一点,我们来实现DatabaseOperations类,它将在每次测试重试之间负责清理数据库:
import android.database.sqlite.SQLiteDatabase
import androidx.test.platform.app.InstrumentationRegistry
import java.io.File
public open class DatabaseOperations {
public open fun clearDatabases() {
val databaseOperations = DatabaseOperations()
val dbFiles = databaseOperations.getAllDatabaseFiles().filterNot { shouldIgnoreFile(it.path) }
dbFiles.forEach { clearDatabase(it, databaseOperations) }
}
private fun shouldIgnoreFile(path: String): Boolean {
val ignoredSuffixes = arrayOf("-journal", "-shm", "-uid", "-wal")
return ignoredSuffixes.any { path.endsWith(it) }
}
private fun clearDatabase(dbFile: File, dbOperations: DatabaseOperations) {
dbOperations.openDatabase(dbFile).use { database ->
val tablesToClear = dbOperations.getTableNames(database).filterNot { it == "room_master_table" }
tablesToClear.forEach { dbOperations.deleteTableContent(database, it) }
}
}
private fun getAllDatabaseFiles(): List<File> {
return InstrumentationRegistry.getInstrumentation().targetContext.let { context ->
context.databaseList().map { context.getDatabasePath(it) }
}
}
private fun openDatabase(databaseFile: File): SQLiteDatabase {
return SQLiteDatabase.openDatabase(databaseFile.absolutePath, null, 0)
}
private fun getTableNames(sqLiteDatabase: SQLiteDatabase): List<String> {
sqLiteDatabase.rawQuery("SELECT name FROM sqlite_master WHERE type IN (?, ?)", arrayOf("table", "view"))
.use { cursor ->
val tableNames = ArrayList<String>()
while (cursor.moveToNext()) {
tableNames.add(cursor.getString(0))
}
return tableNames
}
}
private fun deleteTableContent(sqLiteDatabase: SQLiteDatabase, tableName: String) {
sqLiteDatabase.delete(tableName, null, null)
}
}
因此,RetryRule 类的语句函数将进行一些调整:
private fun statement(base: Statement, description: Description): Statement {
return object : Statement() {
@Throws(Throwable::class)
override fun evaluate() {
val retryAnnotation: Retry? = description.getAnnotation(Retry::class.java)
val retryCount = retryAnnotation?.count ?: count
val databaseOperations = DatabaseOperations() // <~~~~~~ new line
var caughtThrowable: Throwable? = null
for (i in 0 until retryCount) {
try {
System.err.println("${description.displayName}: run #${(i + 1)} started.")
base.evaluate()
return
} catch (t: Throwable) {
System.err.println("${description.displayName}: run #${(i + 1)} failed.")
databaseOperations.clearDatabases() // <~~~~~~ new line
caughtThrowable = t
}
}
throw caughtThrowable ?: IllegalStateException()
}
}
}
总结
该重试,还是不该重试,这是个问题。你的意见如何呢?欢迎在评论区给出你的意见。