当前位置:网站首页>Deep dive kotlin synergy (XV): Test kotlin synergy
Deep dive kotlin synergy (XV): Test kotlin synergy
2022-06-27 04:25:00 【RikkaTheWorld】
Series eBook : Portal
in the majority of cases , Testing a suspended function is no different from testing a normal function . Look at the following FetchUserUseCase.fetchUserData
. By forging Fake( Or simulation Mock) And simple assertions , We can easily test whether it displays the data as expected :
class FetchUserUseCase(
private val repo: UserDataRepository,
) {
suspend fun fetchUserData(): User = coroutineScope {
val name = async {
repo.getName() }
val friends = async {
repo.getFriends() }
val profile = async {
repo.getProfile() }
User(
name = name.await(),
friends = friends.await(),
profile = profile.await()
)
}
}
class FetchUserDataTest {
@Test
fun `should construct user`() = runBlocking {
// given
val repo = FakeUserDataRepository()
val useCase = FetchUserUseCase(repo)
// when
val result = useCase.fetchUserData()
// then
val expectedUser = User(
name = "Ben",
friends = listOf(Friend("some-friend-id-1")),
profile = Profile("Example description")
)
assertEquals(expectedUser, result)
}
class FakeUserDataRepository : UserDataRepository {
override suspend fun getName(): String = "Ben"
override suspend fun getFriends(): List<Friend> =
listOf(Friend("some-friend-id-1"))
override suspend fun getProfile(): Profile =
Profile("Example description")
}
}
The above test function should not be used as a reference standard . There are many ways to express unit tests . I used it here forge fake instead of simulation mock, This will not introduce any third-party libraries ( I personally prefer this way ). I also try to make all the tests as simple as possible , To make them easier to read .
Allied , in many instances , If we need to test the suspend function , In fact, just use runBlocking
And some classic assertion tools . This is what unit testing looks like in many projects , Here is Kt.Academy A unit test case of the background project :
class UserTests : KtAcademyFacadeTest() {
@Test
fun `should modify user details`() = runBlocking {
// given
thereIsUser(aUserToken, aUserId)
// when
facade.updateUserSelf(
aUserToken,
PatchUserSelfRequest(
bio = aUserBio,
bioPl = aUserBioPl,
publicKey = aUserPublicKey,
customImageUrl = aCustomImageUrl
)
)
// then
with(findUser(aUserId)) {
assertEquals(aUserBio, bio)
assertEquals(aUserBioPl, bioPl)
assertEquals(aUserPublicKey, publicKey)
assertEquals(aCustomImageUrl, customImageUrl)
}
}
//...
}
We only use runBlocking
, There is little difference between testing the behavior of suspended and blocked functions .
Test time dependency
But when we want to test the time dependence of functions , The difference arises . for example , Consider the following function :
suspend fun produceCurrentUserSeq(): User {
val profile = repo.getProfile()
val friends = repo.getFriends()
return User(profile, friends)
}
suspend fun produceCurrentUserSym(): User = coroutineScope {
val profile = async {
repo.getProfile() }
val friends = async {
repo.getFriends() }
User(profile.await(), friends.await())
}
Both functions will produce the same result , But the difference is : The first one is produced in sequence , And the second is done at the same time . If you need to get a profile and a friends list 1 second , So the first function needs to be about 2 second , The second function only needs 1 second , How will you test this difference ?
Please note that , Only getProfile
and getFriends
It does take some time for the implementation to make a difference . If they are all instant , The two ways of generating user information are indistinguishable , therefore , We can simulate the scenario of data loading by forging time-consuming functions :
class FakeDelayedUserDataRepository : UserDataRepository {
override suspend fun getProfile(): Profile {
delay(1000)
return Profile("Example description")
}
override suspend fun getFriends(): List<Friend> {
delay(1000)
return listOf(Friend("some-friend-id-1"))
}
}
Now? , You can see the difference in unit tests : call produceCurrentUserSeq
It will cost about 1 The second time , And call produceCurrentUserSym
It takes about 2 The second time . The problem is that we don't want unit testing to take so much time , There are usually thousands of unit tests in our projects , We want all the tests to be performed as quickly as possible . How to have both fish and bear's paws ? The answer is to use virtual time . Next we will introduce kotln-coroutines-test And its StandardTestDispatcher
.
This chapter introduces 1.6 Version of kotlin-coroutines-test Functions and classes . If you are using an older version of this library . in the majority of cases , use runBlockingTest
Instead of runTest
, use TestCoroutinesDispatcher
Instead of StandardTestDispatcher
, use TestCoroutineScope
Instead of TestScope
That's enough . in addition , In the old version advanceTimeBy
Be similar to 1.6 In the above version advanceTimeBy
and runCurrent
.
TestCoroutineScheduler and StandardTestDispatcher
When we call delay
when , Our collaboration was suspended and resumed after a period of time .kotlinx-coroutines-test Medium TestCoroutineScheduler
Can change this behavior , It makes the delay
Operate on virtual time , This is completely simulated , Not dependent on real-time .
fun main() {
val scheduler = TestCoroutineScheduler()
println(scheduler.currentTime) // 0
scheduler.advanceTimeBy(1_000)
println(scheduler.currentTime) // 1000
scheduler.advanceTimeBy(1_000)
println(scheduler.currentTime) // 2000
}
TestCoroutineScheduler
as well as StandardTestDispatcher
、TestScope
and runTest
Still experimental .
To be used in the collaboration process TestCoroutineScheduler
, We should use a scheduler to assist it . The standard choice is StandardTestDispatcher
. Unlike most schedulers , It is not just used to determine which thread the collaboration runs on . Unless we don't advance the time , Otherwise, the process it starts will not run . The most typical way to use lead time is to call advanceUntilIdle
function , It will advance the virtual time , And call all the operations here :
fun main() {
val scheduler = TestCoroutineScheduler()
val testDispatcher = StandardTestDispatcher(scheduler)
CoroutineScope(testDispatcher).launch {
println("Some work 1")
delay(1000)
println("Some work 2")
delay(1000)
println("Coroutine done")
}
println("[${
scheduler.currentTime}] Before")
scheduler.advanceUntilIdle()
println("[${
scheduler.currentTime}] After")
}
// [0] Before
// Some work 1
// Some work 2
// Coroutine done
// [2000] After
StandardTestDispatcher
Will create a... By default TestCoroutineScheduler
, So we don't need to explicitly create it . We can go through scheduler Property to access it .
fun main() {
val dispatcher = StandardTestDispatcher()
CoroutineScope(dispatcher).launch {
println("Some work 1")
delay(1000)
println("Some work 2")
delay(1000)
println("Coroutine done")
}
println("[${
dispatcher.scheduler.currentTime}] Before")
dispatcher.scheduler.advanceUntilIdle()
println("[${
dispatcher.scheduler.currentTime}] After")
}
// [0] Before
// Some work 1
// Some work 2
// Coroutine done
// [2000] After
It is important to , it is to be noted that StandardTestDispatcher
It will not advance the time , If we don't do anything extra , The process will never recover .
fun main() {
val testDispatcher = StandardTestDispatcher()
runBlocking(testDispatcher) {
delay(1)
println("Coroutine done")
}
}
// ( The code runs forever )
Another way to delay is to use advanceTimeBy
And the specific number of milliseconds . This function advances a specific time and performs all operations that occur during this period . This means that if we delay 2ms, All delays are less than 2ms Will be restored . In order to resume the operation scheduled just in the second millisecond , We need an extra call runCurrent
function :
fun main() {
val testDispatcher = StandardTestDispatcher()
CoroutineScope(testDispatcher).launch {
delay(1)
println("Done1")
}
CoroutineScope(testDispatcher).launch {
delay(2)
println("Done2")
}
testDispatcher.scheduler.advanceTimeBy(2) // Done
testDispatcher.scheduler.runCurrent() // Done2
}
Here's how to use advanceTimeBy
and runCurrent
A larger example of .
fun main() {
val testDispatcher = StandardTestDispatcher()
CoroutineScope(testDispatcher).launch {
delay(2)
print("Done")
}
CoroutineScope(testDispatcher).launch {
delay(4)
print("Done2")
}
CoroutineScope(testDispatcher).launch {
delay(6)
print("Done3")
}
for (i in 1..5) {
print(".")
testDispatcher.scheduler.advanceTimeBy(1)
testDispatcher.scheduler.runCurrent()
}
}
// ..Done..Done2.
How its bottom layer works ? When calling delay
when , It checks dispatcher
( with CoroutinuationInterceptor
Key ) Is it implemented Delay
Interface (StandardTestDispatcher
Realized ). For such a scheduler , Will call their scheduleResumeAfterDelay
function , Instead of waiting in real time DefaultDelay
.
To understand that virtual time is truly independent of real time , See the example below . add to Thread.sleep
It doesn't matter StandardTestDispatcher
Cooperation of . Attention is also needed , Yes advanceUntilIdle
It only takes a few milliseconds to call . So it doesn't have to wait for any real time . It will immediately push the virtual time and execute the co process operation .
fun main() {
val dispatcher = StandardTestDispatcher()
CoroutineScope(dispatcher).launch {
delay(1000)
println("Coroutine done")
}
Thread.sleep(Random.nextLong(2000)) // It doesn't matter how many seconds you sleep here
// It doesn't affect the results
val time = measureTimeMillis {
println("[${
dispatcher.scheduler.currentTime}] Before")
dispatcher.scheduler.advanceUntilIdle()
println("[${
dispatcher.scheduler.currentTime}] After")
}
println("Took $time ms")
}
// [0] Before
// Coroutine done
// [1000] After
// Took 15 ms ( Or a smaller number )
In the example above , We use StandardTestDispatcher
And wrap it with a scope . As a substitute , We can use TestScope
, It did the same thing ( It USES CoroutineExceptionHander
To collect all exceptions ). The key lies in , On this scope , We can also use advanceUntileIdle
、 advanceTimeBy
or currentTime
And other properties and functions , All these functions are delegated to the scheduler used by this scope . This is more convenient .
fun main() {
val scope = TestScope()
scope.launch {
delay(1000)
println("First done")
delay(1000)
println("Coroutine done")
}
println("[${
scope.currentTime}] Before") // [0] Before
scope.advanceTimeBy(1000)
scope.runCurrent() // First done
println("[${
scope.currentTime}] Middle") // [1000] Middle
scope.advanceUntilIdle() // Coroutine done
println("[${
scope.currentTime}] After") // [2000] After
}
Later we will learn about StandardTestDispatcher
Often directly in Android For testing ViewModels 、 Presenter、Fragments etc. . We can also use it to test produceCurrentUserSeq
and produceCurrentUserSym
function , The method is to start them in the process , Push time to leisure , And check how much virtual time they spend . However , It's quite complicated . contrary , We should use runTest
, It is designed to solve this problem .
runTest
runTest
yes kotlinx-coroutines-test The most commonly used function in . It USES TestScope
So let's start a coroutine , And immediately push it until it is free . The scope type that wraps it is TestScope
, So we can check at any point currentTime
.
class TestTest {
@Test
fun test1() = runTest {
assertEquals(0, currentTime)
delay(1000)
assertEquals(1000, currentTime)
}
@Test
fun test2() = runTest {
assertEquals(0, currentTime)
coroutineScope {
launch {
delay(1000) }
launch {
delay(1500) }
launch {
delay(2000) }
}
assertEquals(2000, currentTime)
}
}
Let's go back to the function , In the function , We load user data sequentially or simultaneously . Use runTest
, It's easy to test them . Suppose that our fake repository needs... For each function call 1 second , Then sequential processing should require 2 second , Simultaneous processing should only require 1 second . Because we use virtual time , So the test is even ,currentTime
The value of is accurate .
@Test
fun `Should produce user sequentially`() = runTest {
// given
val userDataRepository = FakeDelayedUserDataRepository()
val useCase = ProduceUserUseCase(userDataRepository)
// when
useCase.produceCurrentUserSeq()
// then
assertEquals(2000, currentTime)
}
@Test
fun `Should produce user simultaneously`() = runTest {
// given
val userDataRepository = FakeDelayedUserDataRepository()
val useCase = ProduceUserUseCase(userDataRepository)
// when
useCase.produceCurrentUserSym()
// then
assertEquals(1000, currentTime)
}
Because it is an important use case , So let's take a look at the complete example :
class FetchUserUseCase(
private val repo: UserDataRepository,
) {
suspend fun fetchUserData(): User = coroutineScope {
val name = async {
repo.getName() }
val friends = async {
repo.getFriends() }
val profile = async {
repo.getProfile() }
User(
name = name.await(),
friends = friends.await(),
profile = profile.await()
)
}
}
class FetchUserDataTest {
@Test
fun `should load data concurrently`() = runTest {
// given
val userRepo = FakeUserDataRepository()
val useCase = FetchUserUseCase(userRepo)
// when
useCase.fetchUserData()
// then
assertEquals(1000, currentTime)
}
@Test
fun `should construct user`() = runTest {
// given
val userRepo = FakeUserDataRepository()
val useCase = FetchUserUseCase(userRepo)
// when
val result = useCase.fetchUserData()
// then
val expectedUser = User(
name = "Ben",
friends = listOf(Friend("some-friend-id-1")),
profile = Profile("Example description")
)
assertEquals(expectedUser, result)
}
class FakeUserDataRepository : UserDataRepository {
override suspend fun getName(): String {
delay(1000)
return "Ben"
}
override suspend fun getFriends(): List<Friend> {
delay(1000)
return listOf(Friend("some-friend-id-1"))
}
override suspend fun getProfile(): Profile {
delay(1000)
return Profile("Example description")
}
}
}
interface UserDataRepository {
suspend fun getName(): String
suspend fun getFriends(): List<Friend>
suspend fun getProfile(): Profile
}
data class User(
val name: String,
val friends: List<Friend>,
val profile: Profile
)
data class Friend(val id: String)
data class Profile(val description: String)
UnconfinedTestDispatcher
except StandardTestDispatcher
, We also have UnconfinedTestDispatcher
. The biggest difference between them is that the former will not perform any operation before we use its scheduler . The latter will immediately perform all operations before the first delay of the startup process , This is the code print below “C” Why :
fun main() {
CoroutineScope(StandardTestDispatcher()).launch {
print("A")
delay(1)
print("B")
}
CoroutineScope(UnconfinedTestDispatcher()).launch {
print("C")
delay(1)
print("D")
}
}
// C
stay 1.6 Version of kotlinx-coroutines-test Introduced in runTest
function , Before this version , We use runBlockingTest
, Its behavior is closer to using UnconfinedTestDispatcher.runTest
, therefore , If you want to go directly from runBlockingTest
Migrate to runTest
, The code needs to look like this :
@Test
fun testName() = runTest(UnconfinedTestDispatcher()) {
//...
}
Using simulation
Use in forged objects delay
It's simple but not direct enough . Many developers prefer to call... Directly in test functions delay
. One way is to use mock
simulation :
@Test
fun `should load data concurrently`() = runTest {
// given
val userRepo = mockk<UserDataRepository>()
coEvery {
userRepo.getName() } coAnswers {
delay(600)
aName
}
coEvery {
userRepo.getFriends() } coAnswers {
delay(700)
someFriends
}
coEvery {
userRepo.getProfile() } coAnswers {
delay(800)
aProfile
}
val useCase = FetchUserUseCase(userRepo)
// when
useCase.fetchUserData()
// then
assertEquals(800, currentTime)
}
In the example above , I use the MockK library .
Test function change scheduler
stay Dispatchers Co scheduler Chapter 1 , We introduced the setting details Dispatchers The typical case . for example , We use Dispachers.IO
( Or customize the scheduler ) To call the blocking function , Use Dispachers.Default
To execute CPU Intensive operation . Such functions rarely need to be executed at the same time , So we usually use runBlocking
Testing them is enough . It's very simple , In fact, it is no different from testing blocking functions . for example , Consider the following function :
suspend fun readSave(name: String): GameState =
withContext(Dispatchers.IO) {
reader.readCsvBlocking(name, GameState::class.java)
}
suspend fun calculateModel() =
withContext(Dispatchers.Default) {
model.fit(
dataset = newTrain,
epochs = 10,
batchSize = 100,
verbose = false
)
}
We can use runBlocking
The behavior of wrapping these functions , But how to check whether these functions actually use the scheduler ? If we simulate the function being called , And capture the name of the thread used internally , That can be done .
@Test
fun `should change dispatcher`() = runBlocking {
// given
val csvReader = mockk<CsvReader>()
val startThreadName = "MyName"
var usedThreadName: String? = null
every {
csvReader.readCsvBlocking(
aFileName,
GameState::class.java
)
} coAnswers {
usedThreadName = Thread.currentThread().name
aGameState
}
val saveReader = SaveReader(csvReader)
// when
withContext(newSingleThreadContext(startThreadName)) {
saveReader.readSave(aFileName)
}
// then
assertNotNull(usedThreadName)
val expectedPrefix = "DefaultDispatcher-worker-"
assert(usedThreadName!!.startsWith(expectedPrefix))
}
In the function above , I can't use forgery , because CsvReader
It's a class, not an interface , So I used mock. remember , Scheduler Dispachers.Default
and Dispatchers.IO
Share the same thread pool .
However , In rare cases , We might want to test the time dependency in the function . It's a tricky situation , Because the new scheduler replaces our StandardTestDispatcher
, So we stop working on virtual time , To illustrate this point more clearly , Let's use withContext(Disptachers.IO)
packing fetchUserData
function .
suspend fun fetchUserData() = withContext(Dispatchers.IO) {
val name = async {
userRepo.getName() }
val friends = async {
userRepo.getFriends() }
val profile = async {
userRepo.getProfile() }
User(
name = name.await(),
friends = friends.await(),
profile = profile.await()
)
}
Now? , All the tests we have implemented before will be waiting in real time , currentTime
Will always be 0. To prevent this from happening , The simplest way is to inject the scheduler through the constructor , And replace it in unit tests .
class FetchUserUseCase(
private val userRepo: UserDataRepository,
private val ioDispatcher: CoroutineDispatcher =
Dispatchers.IO
) {
suspend fun fetchUserData() = withContext(ioDispatcher) {
val name = async {
userRepo.getName() }
val friends = async {
userRepo.getFriends() }
val profile = async {
userRepo.getProfile() }
User(
name = name.await(),
friends = friends.await(),
profile = profile.await()
)
}
}
Now? , We should not provide... In unit tests Disptachers.IO
, It should use runTest
Medium StandardTestDispatcher
. We can use ContinuationInterceptor
Key from coroutineContext
Get it .
val testDispatcher = this
.coroutineContext[ContinuationInterceptor]
as CoroutineDispatcher
val useCase = FetchUserUseCase(
userRepo = userRepo,
ioDispatcher = testDispatcher,
)
Another possibility is to ioDispacher
Convert to CoroutineContext
, And use in unit test EmptyCoroutineContext
Replace it :
val useCase = FetchUserUseCase(
userRepo = userRepo,
ioDispatcher = EmptyCoroutineContext,
)
Test what happens during function execution
Imagine a function : It displays a progress bar during execution , Then hide it :
suspend fun sendUserData() {
val userData = database.getUserData()
progressBarVisible.value = true
userRepository.sendUserData(userData)
progressBarVisible.value = false
}
If we only check the final result , We cannot verify whether the progress bar has changed its state during execution . under these circumstances , The trick is to start this function in a new coroutine , And control the virtual time externally . Be careful ,runTest
Use StandardTestDispatcher
The scheduler creates a coroutine , And advance the time to idle ( Use advanceUntilIde
function ). This means that once the parent process starts waiting for the child process , The time of the subprocess will start .
@Test
fun `should show progress bar when sending data`() = runTest {
// given
val database = FakeDatabase()
val vm = UserViewModel(database)
// when
launch {
vm.sendUserData()
}
// then
assertEquals(false, vm.progressBarVisible.value)
// when
advanceTimeBy(1000)
// then
assertEquals(false, vm.progressBarVisible.value)
// when
runCurrent()
// then
assertEquals(true, vm.progressBarVisible.value)
// when
advanceUntilIdle()
// then
assertEquals(false, vm.progressBarVisible.value)
}
Thanks to runCurrent
, We can accurately check the change of some values .
If we use delay
, A similar effect can be achieved . It's like having two separate processes : One is doing a task , The other one is checking whether the first one behaves normally .
@Test
fun `should show progress bar when sending data`() =
runTest {
val database = FakeDatabase()
val vm = UserViewModel(database)
launch {
vm.showUserData()
}
// then
assertEquals(false, vm.progressBarVisible.value)
delay(1000)
assertEquals(true, vm.progressBarVisible.value)
delay(1000)
assertEquals(false, vm.progressBarVisible.value)
}
Use advanceTimeBy
Such explicit functions are considered better than using delay
Higher readability .
Test the function that starts the new coroutine
The process needs to start somewhere . On the back end , They are usually used by the framework we use ( for example Spring or Ktor) start-up , But sometimes we may need to construct a scope ourselves , And start the coordination process on it .
@Scheduled(fixedRate = 5000)
fun sendNotifications() {
notificationsScope.launch {
val notifications = notificationsRepository
.notificationsToSend()
for (notification in notifications) {
launch {
notificationsService.send(notification)
notificationsRepository
.markAsSent(notification.id)
}
}
}
}
If the notification is sent concurrently , How do we test sendNotification
? Again , In unit test , We need to use StandardTestDispatcher
As part of the scope . We should also add some delay to invoke send
and markAsSent
.
@Test
fun testSendNotifications() {
// given
val notifications = List(100) {
Notification(it) }
val repo = FakeNotificationsRepository(
delayMillis = 200,
notifications = notifications,
)
val service = FakeNotificationsService(
delayMillis = 300,
)
val testScope = TestScope()
val sender = NotificationsSender(
notificationsRepository = repo,
notificationsService = service,
notificationsScope = testScope
)
// when
sender.sendNotifications()
testScope.advanceUntilIdle()
// Then all the notifications will be sent and marked
assertEquals(
notifications.toSet(),
service.notificationsSent.toSet()
)
assertEquals(
notifications.map {
it.id }.toSet(),
repo.notificationsMarkedAsSent.toSet()
)
// All notifications will be sent concurrently
assertEquals(700, testScope.currentTime)
}
Be careful , There is no need to use... In the above code runBlocking
. sendNotifications
and advanceUntilIdle
Are regular functions .
Replace the main regulator
There is no main function in the unit test . It means , If we try to use it , Our test will fail . appear “Module with the Main dispatcher is missing” abnormal . On the other hand , Each injection into the main thread will be very difficult , therefore kotlinx-coroutines-test Library in Dispatchers I have provided setMain
spread function .
class MainPresenter(
private val mainView: MainView,
private val dataRepository: DataRepo
) {
suspend fun onCreate() = coroutineScope {
launch(Dispatchers.Main) {
val data = dataRepository.fetchData()
mainView.show(data)
}
}
}
class FakeMainView : MainView {
var dispatchersUsedToShow: List<CoroutineContext?> =
emptyList()
override suspend fun show(data: Data) {
dispatchersUsedToShow +=
coroutineContext[ContinuationInterceptor]
}
}
class FakeDataRepo : DataRepo {
override suspend fun fetchData(): Data {
delay(1000)
return Data()
}
}
class SomeTest {
private val mainDispatcher = Executors
.newSingleThreadExecutor()
.asCoroutineDispatcher()
@Before
fun setup() {
Dispatchers.setMain(mainDispatcher)
}
@After
fun tearDown() {
Dispatchers.resetMain()
}
@Test
fun testSomeUI() = runBlocking {
// given
val view = FakeMainView()
val repo = FakeDataRepo()
val presenter = MainPresenter(view, repo)
// when
presenter.onCreate()
// show stay Dispatchers.Main Called on
assertEquals(
listOf(Dispatchers.Main),
view.dispatchersUsedToShow
)
}
}
Notice the example above . stay assertEquals in , I compared dispatrchersUsedToShow
and Dispatchers.Main
. And the reason for that is mainDispatcher
Is set to delegate Dispatchers.Main
.
We often do this on base classes that are extensions of all unit tests setup function ( with @Before
or @BeforeEach
Function of ) Define the master tuner on the . therefore , We are always sure that we can Dispatcher.Main
Run the collaboration on . We should also use Dispatchers.resetMain()
Reset to initial state .
Test the startup process Android function
stay Android On , We usually ViewModels
、Presenters
、Fragments
or Activities
Start the process . These are very important classes , We should test it . Look at the following MainViewModel
Realization :
class MainViewModel(
private val userRepo: UserRepository,
private val newsRepo: NewsRepository,
) : BaseViewModel() {
private val _userName: MutableLiveData<String> =
MutableLiveData()
val userName: LiveData<String> = _userName
private val _news: MutableLiveData<List<News>> = MutableLiveData()
val news: LiveData<List<News>> = _news
private val _progressVisible: MutableLiveData<Boolean> =
MutableLiveData()
val progressVisible: LiveData<Boolean> = _progressVisible
fun onCreate() {
viewModelScope.launch {
val user = userRepo.getUser()
_userName.value = user.name
}
viewModelScope.launch {
_progressVisible.value = true
val news = newsRepo.getNews()
.sortedByDescending {
it.date }
_news.value = news
_progressVisible.value = false
}
}
}
Is likely to , We will create our own scope to replace viewModelScope
, It is also possible that we use Presenter、 Activity Or other classes instead of ViewModel. This has nothing to do with our example , In the class that tests each startup coroutine , We should use StandardTestDispatcher
As part of the scope , before , We can inject a different scope through dependency injection , But now there is a simpler way : stay Android On , We use Dispatcher.Main
As the default scheduler , And we can use StandardTestDispatcher
Replace it , Thank you Dispatchers.setMain
function .
private val testDispatcher = StandardTestDispatcher()
@Before
fun setUp() {
Dispatchers.setMain(testDispatcher)
}
@After
fun tearDown() {
Dispatchers.resetMain()
}
After setting the main regulator in this way , onCreate
The collaboration will take place in testDispatcher
Up operation , So we can control their time . We can use advanceTimeBy
Function to pretend that it's been a while . We can also use advanceUntilIdle
To restore all the processes , Until they're done .
class MainViewModelTests {
private lateinit var scheduler: TestCoroutineScheduler
private lateinit var viewModel: MainViewModel
@BeforeEach
fun setUp() {
scheduler = TestCoroutineScheduler()
Dispatchers.setMain(StandardTestDispatcher(scheduler))
viewModel = MainViewModel(
userRepo = FakeUserRepository(aName),
newsRepo = FakeNewsRepository(someNews)
)
}
@AfterEach
fun tearDown() {
Dispatchers.resetMain()
viewModel.onCleared()
}
@Test
fun `should show user name and sorted news`() {
// when
viewModel.onCreate()
scheduler.advanceUntilIdle()
// then
assertEquals(aName, viewModel.userName.value)
val someNewsSorted = listOf(News(date1), News(date2), News(date3))
assertEquals(someNewsSorted, viewModel.news.value)
}
@Test
fun `should show progress bar when loading news`() {
// given
assertEquals(null, viewModel.progressVisible.value)
// when
viewModel.onCreate()
// then
assertEquals(false, viewModel.progressVisible.value)
// when
scheduler.advanceTimeBy(200)
// then
assertEquals(true, viewModel.progressVisible.value)
// when
scheduler.runCurrent()
// then
assertEquals(false, viewModel.progressVisible.value)
}
@Test
fun `user and news are called concurrently`() {
// when
viewModel.onCreate()
scheduler.advanceUntilIdle()
// then
assertEquals(300, testDispatcher.currentTime)
}
class FakeUserRepository(
private val name: String
) : UserRepository {
override suspend fun getUser(): UserData {
delay(300)
return UserData(name)
}
}
class FakeNewsRepository(
private val news: List<News>
) : NewsRepository {
override suspend fun getNews(): List<News> {
delay(200)
return news
}
}
}
Use rules to set up the scheduler
JUnit 4 Allow us to define some rules . These classes contain logic that should be invoked in some test class lifecycle Events . for example , A rule can define what needs to be done before and after all tests , So in our case, we can use it to set up our scheduler , And then clean it up . Here is a good implementation of this rule :
class MainCoroutineRule : TestWatcher() {
lateinit var scheduler: TestCoroutineScheduler
private set
lateinit var dispatcher: TestDispatcher
private set
override fun starting(description: Description) {
scheduler = TestCoroutineScheduler()
dispatcher = StandardTestDispatcher(scheduler)
Dispatchers.setMain(dispatcher)
}
override fun finished(description: Description) {
Dispatchers.resetMain()
}
}
This rule needs to be extended TestWatcher
, It provides a way to test the lifecycle , For example, we can rewrite starting
and finished
. It combines TestCoroutineScheduler
and TestDispatcher
, Before each test in a class that uses this rule , TestDispatcher
Will be set to Main, After each test , The main adjuster will be reset . We can use the rule interface scheduler Property access scheduler .
class MainViewModelTests {
@get:Rule
var mainCoroutineRule = MainCoroutineRule()
// ...
@Test
fun `should show user name and sorted news`() {
// when
viewModel.onCreate()
mainCoroutineRule.scheduler.advanceUntilIdle()
// then
assertEquals(aName, viewModel.userName.value)
val someNewsSorted =
listOf(News(date1), News(date2), News(date3))
assertEquals(someNewsSorted, viewModel.news.value)
}
@Test
fun `should show progress bar when loading news`() {
// given
assertEquals(null, viewModel.progressVisible.value)
// when
viewModel.onCreate()
// then
assertEquals(true, viewModel.progressVisible.value)
// when
mainCoroutineRule.scheduler.advanceTimeBy(200)
// then
assertEquals(false, viewModel.progressVisible.value)
}
@Test
fun `user and news are called concurrently`() {
// when
viewModel.onCreate()
mainCoroutineRule.scheduler.advanceUntilIdle()
// then
assertEquals(300, mainCoroutineRule.currentTime)
}
}
If you want to go directly to MainCoroutineRule
On the call advanceUntileIdle
、advanceTimeBy
、runCurrent
and currentTime
, You can define them as extension functions and attributes .
This kind of test Kotlin The method of synergy is Android It is quite common . It's even in Google's Codelabs Is recommended in (Android Test collaboration )( The present is based on the old kotlinx-coroutines-test API), It is associated with JUnit5 similar , We can define an extension :
@ExperimentalCoroutinesApi
class MainCoroutineExtension:
BeforeEachCallback, AfterEachCallback {
lateinit var scheduler: TestCoroutineScheduler
private set
lateinit var dispatcher: TestDispatcher
private set
override fun beforeEach(context: ExtensionContext?) {
scheduler = TestCoroutineScheduler()
dispatcher = StandardTestDispatcher(scheduler)
Dispatchers.setMain(dispatcher)
}
override fun afterEach(context: ExtensionContext?) {
Dispatchers.resetMain()
}
}
Use MainCoroutineExtension
And use MainCoroutineRule
The rules are almost the same . The difference is , We need to use @JvmField
and @RegisterExtension
Instead of @get:Rule
notes .
@JvmField
@RegisterExtension
var mainCoroutineExtension = MainCoroutineExtension()
summary
In this chapter , We talked about Kotlin The most important use case of concurrent unit testing . There are some techniques we need to know , But ultimately our tests can be very elegant , Everything can be easily tested . I hope you are using Kotlin Coroutines This chapter can inspire you to write good tests in your application .
边栏推荐
- Mysql database foundation: DQL data query language
- [station B up dr_can learning notes] Kalman filter 3
- 021 C语言基础:递归,可变参数
- 如何系统学习LabVIEW?
- Almost because of json Stringify lost his bonus
- Matlab | drawing of three ordinate diagram based on block diagram layout
- 微服务系统设计——微服务监控与系统资源监控设计
- 2022-06-26:以下golang代码输出什么?A:true;B:false;C:编译错误。 package main import “fmt“ func main() { type
- Microservice system design -- distributed transaction service design
- Microservice system design -- unified authentication service design
猜你喜欢
文旅灯光秀打破时空限制,展现景区夜游魅力
1.5 use of CONDA
微服务系统设计——服务熔断和降级设计
【B站UP DR_CAN学习笔记】Kalman滤波3
Microservice system design -- microservice monitoring and system resource monitoring design
乐得瑞LDR6035 USB-C接口设备支持可充电可OTG传输数据方案。
Penetration test - directory traversal vulnerability
Is the truth XX coming? Why are test / development programmers unwilling to work overtime? This is a crazy state
Microservice system design -- service registration, discovery and configuration design
QChart笔记2: 添加鼠标悬停显示
随机推荐
Games101 job 7 improvement - implementation process of micro surface material
Microservice system design -- distributed lock service design
QChart笔记2: 添加鼠标悬停显示
Ldr6028 OTG data transmission scheme for mobile devices while charging
016 C语言基础:C语言枚举类型
011 C language basics: C scope rules
013 basics of C language: C pointer
Building lightweight target detection based on mobilenet-yolov4
微服务系统设计——消息缓存服务设计
022 C语言基础:C内存管理与C命令行参数
Qchart note 2: add rollover display
012 C语言基础:C数组
007 basics of C language: C operator
MATLAB | 三个趣的圆相关的数理性质可视化
015 basics of C language: C structure and common body
DAST black box vulnerability scanner part 6: operation (final)
Cache comprehensive project - seckill architecture
Microservice system design -- distributed transaction service design
Is the truth XX coming? Why are test / development programmers unwilling to work overtime? This is a crazy state
Mobilenet series (4): mobilenetv3 network details