1、介绍
DataStore是Jetpack提供的一种数据存储解决方案,提过两种不同实现:Preferences DataStore 和 Proto DataStore。它的数据是通过文件形式存储的,根据其源码可知,其文件存储在全局文件夹datastore下,代码如下:
/**
* Generate the File object for Preferences DataStore based on the provided context and name. The
* file is in the [this.applicationContext.filesDir] + "datastore/" subdirectory with [name].
* This is public to allow for testing and backwards compatibility (e.g. if moving from the
* `preferencesDataStore` delegate or context.createDataStore to
* PreferencesDataStoreFactory).
*
* Do NOT use the file outside of DataStore.
*
* @this the context of the application used to get the files directory
* @name the name of the preferences
*/
public fun Context.preferencesDataStoreFile(name: String): File =
this.dataStoreFile("$name.preferences_pb")
public fun Context.dataStoreFile(fileName: String): File =
File(applicationContext.filesDir, "datastore/$fileName")
2、UnitTest测试问题
在我们编写测试代码的时候,如果我们测试过程中涉及到了DataStore数据操作,在运行单个测试用例时,不会察觉到数据的问题,但当我们运行多个测试用例时,如果每个测试用例涉及到了相同的DataStore数据操作,则会发现后面的个别用例测试运行达不到预期结果。这是因为其整个对象在运行测试用例过程中,所引用都是同一个对象,数据并没有清除重置。
在我们整个工程里面,涉及到DataStore操作时,其对象是通过单例创建出来。有两处会导致:
1、我们自己代码创建生成的DataStore对象,如:
val Context.netDataStore: DataStore<Preferences> by preferencesDataStore(
name = SP_NET_TOKEN
)
2、PreferenceDataStoreDelegate 中源码部分:
internal class PreferenceDataStoreSingletonDelegate internal constructor(
private val name: String,
private val corruptionHandler: ReplaceFileCorruptionHandler<Preferences>?,
private val produceMigrations: (Context) -> List<DataMigration<Preferences>>,
private val scope: CoroutineScope
) : ReadOnlyProperty<Context, DataStore<Preferences>> {
private val lock = Any()
@GuardedBy("lock")
@Volatile
private var INSTANCE: DataStore<Preferences>? = null
/**
* Gets the instance of the DataStore.
*
* @param thisRef must be an instance of [Context]
* @param property not used
*/
override fun getValue(thisRef: Context, property: KProperty<*>): DataStore<Preferences> {
return INSTANCE ?: synchronized(lock) {
if (INSTANCE == null) {
val applicationContext = thisRef.applicationContext
INSTANCE = PreferenceDataStoreFactory.create(
corruptionHandler = corruptionHandler,
migrations = produceMigrations(applicationContext),
scope = scope
) {
applicationContext.preferencesDataStoreFile(name)
}
}
INSTANCE!!
}
}
}
3、UnitTest中的解决方案:
通过源码可以知道,我们可以通过直接删除其文件夹形式删除数据。这一方案在一些博客上也有推荐。但这种操作方式在运行单个测试用例中,可以这样使用。而在多个测试用例中则无法达到清除数据的效果。原因除了其在测试全局是单例外,还因为其数据更新过程中,是存储在内存中。
当在我们更新数据的时候,其数据会同步写入到data中,它是一个flow数据,它内部又是通过一个状态流发送数据的。当我们调用updateData更新数据的时候,它也同步将数据发送到了这个状态流中,代码如下:
internal class SingleProcessDataStore<T>(
private val produceFile: () -> File,
private val serializer: Serializer<T>,
/**
* The list of initialization tasks to perform. These tasks will be completed before any data
* is published to the data and before any read-modify-writes execute in updateData. If
* any of the tasks fail, the tasks will be run again the next time data is collected or
* updateData is called. Init tasks should not wait on results from data - this will
* result in deadlock.
*/
initTasksList: List<suspend (api: InitializerApi<T>) -> Unit> = emptyList(),
private val corruptionHandler: CorruptionHandler<T> = NoOpCorruptionHandler<T>(),
private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
) : DataStore<T> {
override val data: Flow<T> = flow {
val currentDownStreamFlowState = downstreamFlow.value
if (currentDownStreamFlowState !is Data) {
// We need to send a read request because we don't have data yet.
actor.offer(Message.Read(currentDownStreamFlowState))
}
emitAll(
downstreamFlow.dropWhile {
if (currentDownStreamFlowState is Data<T> ||
currentDownStreamFlowState is Final<T>
) {
// We don't need to drop any Data or Final values.
false
} else {
// we need to drop the last seen state since it was either an exception or
// wasn't yet initialized. Since we sent a message to actor, we *will* see a
// new value.
it === currentDownStreamFlowState
}
} .map {
when (it) {
is ReadException<T> -> throw it.readException
is Final<T> -> throw it.finalException
is Data<T> -> it.value
is UnInitialized -> error(
"This is a bug in DataStore. Please file a bug at: " +
"https://issuetracker.google.com/issues/new?" +
"component=907884&template=1466542"
)
}
}
)
}
override suspend fun updateData(transform: suspend (t: T) -> T): T {
/**
* The states here are the same as the states for reads. Additionally we send an ack that
* the actor *must* respond to (even if it is cancelled).
*/
val ack = CompletableDeferred<T>()
val currentDownStreamFlowState = downstreamFlow.value
val updateMsg =
Message.Update(transform, ack, currentDownStreamFlowState, coroutineContext)
actor.offer(updateMsg)
return ack.await()
}
}
也就导致了即使我们在每一个测试用例运行前删除了文件,但在我们运行多个测试用例的时候,测试用例中的数据一直使用的是内存中的数据。也就是说,使用到的DataStore数据,事实上都是上一个测试用例跑完后赋值的数据,而不是初始数据。
internal class SingleProcessDataStore<T>(
private val produceFile: () -> File,
private val serializer: Serializer<T>,
/**
* The list of initialization tasks to perform. These tasks will be completed before any data
* is published to the data and before any read-modify-writes execute in updateData. If
* any of the tasks fail, the tasks will be run again the next time data is collected or
* updateData is called. Init tasks should not wait on results from data - this will
* result in deadlock.
*/
initTasksList: List<suspend (api: InitializerApi<T>) -> Unit> = emptyList(),
private val corruptionHandler: CorruptionHandler<T> = NoOpCorruptionHandler<T>(),
private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
) : DataStore<T> {
override val data: Flow<T> = flow {
val currentDownStreamFlowState = downstreamFlow.value
if (currentDownStreamFlowState !is Data) {
// We need to send a read request because we don't have data yet.
actor.offer(Message.Read(currentDownStreamFlowState))
}
emitAll(
downstreamFlow.dropWhile {
if (currentDownStreamFlowState is Data<T> ||
currentDownStreamFlowState is Final<T>
) {
// We don't need to drop any Data or Final values.
false
} else {
// we need to drop the last seen state since it was either an exception or
// wasn't yet initialized. Since we sent a message to actor, we *will* see a
// new value.
it === currentDownStreamFlowState
}
} .map {
when (it) {
is ReadException<T> -> throw it.readException
is Final<T> -> throw it.finalException
is Data<T> -> it.value
is UnInitialized -> error(
"This is a bug in DataStore. Please file a bug at: " +
"https://issuetracker.google.com/issues/new?" +
"component=907884&template=1466542"
)
}
}
)
}
override suspend fun updateData(transform: suspend (t: T) -> T): T {
/**
* The states here are the same as the states for reads. Additionally we send an ack that
* the actor *must* respond to (even if it is cancelled).
*/
val ack = CompletableDeferred<T>()
val currentDownStreamFlowState = downstreamFlow.value
val updateMsg =
Message.Update(transform, ack, currentDownStreamFlowState, coroutineContext)
actor.offer(updateMsg)
return ack.await()
}
}
3、UnitTest测试解决方案
所以,关于DataStore的最佳测试实践,是在运行每个测试用例前,将数据重置。比如重新赋值成最初值来解决数据初始化的问题。