Skip to content

Instantly share code, notes, and snippets.

@peculiaruc
Created February 1, 2021 22:29
Show Gist options
  • Save peculiaruc/2e63953249c1af8d731246f737fec588 to your computer and use it in GitHub Desktop.
Save peculiaruc/2e63953249c1af8d731246f737fec588 to your computer and use it in GitHub Desktop.
Code coverage for oppia.android.domain.audio/AudioPlayerControllerTest
package org.oppia.android.domain.audio
2
3 import android.app.Application
4 import android.content.Context
5 import android.net.Uri
6 import androidx.lifecycle.Observer
7 import androidx.test.core.app.ApplicationProvider
8 import androidx.test.ext.junit.runners.AndroidJUnit4
9 import com.google.common.truth.Truth.assertThat
10 import dagger.BindsInstance
11 import dagger.Component
12 import dagger.Module
13 import dagger.Provides
14 import org.junit.Before
15 import org.junit.Rule
16 import org.junit.Test
17 import org.junit.runner.RunWith
18 import org.mockito.ArgumentCaptor
19 import org.mockito.Captor
20 import org.mockito.Mock
21 import org.mockito.Mockito.atLeastOnce
22 import org.mockito.Mockito.verify
23 import org.mockito.junit.MockitoJUnit
24 import org.mockito.junit.MockitoRule
25 import org.oppia.android.domain.audio.AudioPlayerController.PlayProgress
26 import org.oppia.android.domain.audio.AudioPlayerController.PlayStatus
27 import org.oppia.android.domain.oppialogger.LogStorageModule
28 import org.oppia.android.testing.FakeExceptionLogger
29 import org.oppia.android.testing.TestCoroutineDispatchers
30 import org.oppia.android.testing.TestDispatcherModule
31 import org.oppia.android.testing.TestLogReportingModule
32 import org.oppia.android.util.caching.CacheAssetsLocally
33 import org.oppia.android.util.data.AsyncResult
34 import org.oppia.android.util.logging.EnableConsoleLog
35 import org.oppia.android.util.logging.EnableFileLog
36 import org.oppia.android.util.logging.GlobalLogLevel
37 import org.oppia.android.util.logging.LogLevel
38 import org.robolectric.Shadows
39 import org.robolectric.annotation.Config
40 import org.robolectric.annotation.LooperMode
41 import org.robolectric.shadows.ShadowMediaPlayer
42 import org.robolectric.shadows.util.DataSource
43 import java.io.IOException
44 import javax.inject.Inject
45 import javax.inject.Singleton
46 import kotlin.reflect.KClass
47 import kotlin.reflect.full.cast
48 import kotlin.test.fail
49
50 /** Tests for [AudioPlayerControllerTest]. */
51 @RunWith(AndroidJUnit4::class)
52 @LooperMode(LooperMode.Mode.PAUSED)
53 @Config(manifest = Config.NONE)
54 class AudioPlayerControllerTest {
55
56 @Rule
57 @JvmField
58 val mockitoRule: MockitoRule = MockitoJUnit.rule()
59
60 @Mock
61 lateinit var mockAudioPlayerObserver: Observer<AsyncResult<PlayProgress>>
62
63 @Captor
64 lateinit var audioPlayerResultCaptor:
65 ArgumentCaptor<AsyncResult<PlayProgress>>
66
67 @Inject
68 lateinit var context: Context
69
70 @Inject
71 lateinit var audioPlayerController: AudioPlayerController
72
73 @Inject
74 lateinit var fakeExceptionLogger: FakeExceptionLogger
75
76 @Inject
77 lateinit var testCoroutineDispatchers: TestCoroutineDispatchers
78
79 private lateinit var shadowMediaPlayer: ShadowMediaPlayer
80
81 private val TEST_URL = "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3"
82 private val TEST_URL2 = "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-2.mp3"
83 private val TEST_FAIL_URL = "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-2"
84
85 @Before
86 fun setUp() {
87 setUpTestApplicationComponent()
88 addMediaInfo()
89 shadowMediaPlayer = Shadows.shadowOf(audioPlayerController.getTestMediaPlayer())
90 shadowMediaPlayer.dataSource = DataSource.toDataSource(context, Uri.parse(TEST_URL))
91 }
92
93 @Test
94 fun testController_initializePlayer_invokePrepared_reportsSuccessfulInit() {
95 audioPlayerController.initializeMediaPlayer()
96 audioPlayerController.changeDataSource(TEST_URL)
97
98 shadowMediaPlayer.invokePreparedListener()
99
100 assertThat(shadowMediaPlayer.isPrepared).isTrue()
101 assertThat(shadowMediaPlayer.isReallyPlaying).isFalse()
102 }
103
104 @Test
105 fun testController_preparePlayer_invokePlay_checkIsPlaying() {
106 arrangeMediaPlayer()
107
108 audioPlayerController.play()
109
110 assertThat(shadowMediaPlayer.isReallyPlaying).isTrue()
111 }
112
113 @Test
114 fun testController_preparePlayer_invokePause_checkNotIsPlaying() {
115 arrangeMediaPlayer()
116
117 audioPlayerController.pause()
118
119 assertThat(shadowMediaPlayer.isReallyPlaying).isFalse()
120 }
121
122 @Test
123 fun testController_preparePlayer_invokeSeekTo_hasCorrectProgress() {
124 arrangeMediaPlayer()
125
126 audioPlayerController.seekTo(500)
127 testCoroutineDispatchers.runCurrent()
128
129 assertThat(shadowMediaPlayer.currentPositionRaw).isEqualTo(500)
130 }
131
132 @Test
133 fun testController_preparePlayer_releaseMediaPlayer_hasEndState() {
134 arrangeMediaPlayer()
135
136 audioPlayerController.releaseMediaPlayer()
137
138 assertThat(shadowMediaPlayer.state).isEqualTo(ShadowMediaPlayer.State.END)
139 }
140
141 @Test
142 fun testController_preparePlayer_invokePrepare_capturesPreparedState() {
143 arrangeMediaPlayer()
144
145 verify(mockAudioPlayerObserver, atLeastOnce()).onChanged(audioPlayerResultCaptor.capture())
146 assertThat(audioPlayerResultCaptor.value.isSuccess()).isTrue()
147 assertThat(audioPlayerResultCaptor.value.getOrThrow().type).isEqualTo(PlayStatus.PREPARED)
148 }
149
150 @Test
151 fun testController_releasePlayer_initializePlayer_capturesPendingState() {
152 audioPlayerController.initializeMediaPlayer()
153
154 audioPlayerController.releaseMediaPlayer()
155 audioPlayerController.initializeMediaPlayer().observeForever(mockAudioPlayerObserver)
156 audioPlayerController.changeDataSource(TEST_URL)
157
158 verify(mockAudioPlayerObserver, atLeastOnce()).onChanged(audioPlayerResultCaptor.capture())
159 assertThat(audioPlayerResultCaptor.value.isPending()).isTrue()
160 }
161
162 @Test
163 fun tesObserver_preparePlayer_invokeCompletion_capturesCompletedState() {
164 arrangeMediaPlayer()
165
166 shadowMediaPlayer.invokeCompletionListener()
167
168 verify(mockAudioPlayerObserver, atLeastOnce()).onChanged(audioPlayerResultCaptor.capture())
169 assertThat(audioPlayerResultCaptor.value.isSuccess()).isTrue()
170 assertThat(audioPlayerResultCaptor.value.getOrThrow().type).isEqualTo(PlayStatus.COMPLETED)
171 assertThat(audioPlayerResultCaptor.value.getOrThrow().position).isEqualTo(0)
172 }
173
174 @Test
175 fun testObserver_preparePlayer_invokeChangeDataSource_capturesPendingState() {
176 arrangeMediaPlayer()
177
178 audioPlayerController.changeDataSource(TEST_URL2)
179
180 verify(mockAudioPlayerObserver, atLeastOnce()).onChanged(audioPlayerResultCaptor.capture())
181 assertThat(audioPlayerResultCaptor.value.isPending()).isTrue()
182 }
183
184 @Test
185 fun testObserver_preparePlayer_invokeChangeDataSourceAfterPlay_capturesPendingState() {
186 arrangeMediaPlayer()
187
188 audioPlayerController.play()
189 audioPlayerController.changeDataSource(TEST_URL2)
190
191 verify(mockAudioPlayerObserver, atLeastOnce()).onChanged(audioPlayerResultCaptor.capture())
192 assertThat(audioPlayerResultCaptor.value.isPending()).isTrue()
193 }
194
195 @Test
196 fun testObserver_preparePlayer_invokePlay_capturesPlayingState() {
197 arrangeMediaPlayer()
198
199 audioPlayerController.play()
200 testCoroutineDispatchers.runCurrent()
201
202 verify(mockAudioPlayerObserver, atLeastOnce()).onChanged(audioPlayerResultCaptor.capture())
203 assertThat(audioPlayerResultCaptor.value.isSuccess()).isTrue()
204 assertThat(audioPlayerResultCaptor.value.getOrThrow().type).isEqualTo(PlayStatus.PLAYING)
205 }
206
207 @Test
208 fun testObserver_preparePlayer_invokePlayAndAdvance_capturesManyPlayingStates() {
209 arrangeMediaPlayer()
210
211 // Wait for 1 second for the player to enter a playing state, then forcibly trigger completion.
212 audioPlayerController.play()
213 testCoroutineDispatchers.advanceTimeBy(1000)
214 shadowMediaPlayer.invokeCompletionListener()
215
216 verify(mockAudioPlayerObserver, atLeastOnce()).onChanged(audioPlayerResultCaptor.capture())
217 val results = audioPlayerResultCaptor.allValues
218 val pendingIndex = results.indexOfLast { it.isPending() }
219 val preparedIndex = results.indexOfLast { it.hasStatus(PlayStatus.PREPARED) }
220 val playingIndex = results.indexOfLast { it.hasStatus(PlayStatus.PLAYING) }
221 val completedIndex = results.indexOfLast { it.hasStatus(PlayStatus.COMPLETED) }
222 // Verify that there are at least 4 statuses: pending, prepared, playing, and completed, and in
223 // that order.
224 assertThat(results.size).isGreaterThan(4)
225 assertThat(pendingIndex).isLessThan(preparedIndex)
226 assertThat(preparedIndex).isLessThan(playingIndex)
227 assertThat(playingIndex).isLessThan(completedIndex)
228 }
229
230 @Test
231 fun testObserver_preparePlayer_invokePause_capturesPausedState() {
232 arrangeMediaPlayer()
233
234 audioPlayerController.play()
235 audioPlayerController.pause()
236
237 verify(mockAudioPlayerObserver, atLeastOnce()).onChanged(audioPlayerResultCaptor.capture())
238 assertThat(audioPlayerResultCaptor.value.isSuccess()).isTrue()
239 assertThat(audioPlayerResultCaptor.value.getOrThrow().type).isEqualTo(PlayStatus.PAUSED)
240 }
241
242 @Test
243 fun testObserver_preparePlayer_invokePrepared_capturesCorrectPosition() {
244 arrangeMediaPlayer()
245
246 verify(mockAudioPlayerObserver, atLeastOnce()).onChanged(audioPlayerResultCaptor.capture())
247 assertThat(audioPlayerResultCaptor.value.getOrThrow().position).isEqualTo(0)
248 }
249
250 @Test
251 fun testObserver_preparePlayer_invokeSeekTo_capturesCorrectPosition() {
252 arrangeMediaPlayer()
253
254 audioPlayerController.seekTo(500)
255 testCoroutineDispatchers.runCurrent()
256 audioPlayerController.play()
257 testCoroutineDispatchers.runCurrent()
258
259 verify(mockAudioPlayerObserver, atLeastOnce()).onChanged(audioPlayerResultCaptor.capture())
260 assertThat(audioPlayerResultCaptor.value.getOrThrow().position).isEqualTo(500)
261 }
262
263 @Test
264 fun testObserver_preparePlayer_invokePlay_capturesCorrectDuration() {
265 arrangeMediaPlayer()
266
267 audioPlayerController.play()
268
269 verify(mockAudioPlayerObserver, atLeastOnce()).onChanged(audioPlayerResultCaptor.capture())
270 assertThat(audioPlayerResultCaptor.value.getOrThrow().duration).isEqualTo(2000)
271 }
272
273 @Test
274 fun testObserver_preparePlayer_invokeChangeDataSource_capturesCorrectPosition() {
275 arrangeMediaPlayer()
276
277 audioPlayerController.seekTo(500)
278 audioPlayerController.changeDataSource(TEST_URL2)
279 shadowMediaPlayer.invokePreparedListener()
280 audioPlayerController.play()
281
282 verify(mockAudioPlayerObserver, atLeastOnce()).onChanged(audioPlayerResultCaptor.capture())
283 assertThat(audioPlayerResultCaptor.value.getOrThrow().position).isEqualTo(0)
284 }
285
286 @Test
287 fun testObserver_observeInitPlayer_releasePlayer_initPlayer_checkNoNewUpdates() {
288 arrangeMediaPlayer()
289
290 audioPlayerController.releaseMediaPlayer()
291 audioPlayerController.initializeMediaPlayer()
292
293 verify(mockAudioPlayerObserver, atLeastOnce()).onChanged(audioPlayerResultCaptor.capture())
294 // If the observer was still getting updates, the result would be pending
295 assertThat(audioPlayerResultCaptor.value.isSuccess()).isTrue()
296 assertThat(audioPlayerResultCaptor.value.getOrThrow().type).isEqualTo(PlayStatus.PREPARED)
297 }
298
299 @Test
300 fun testScheduling_preparePlayer_invokePauseAndAdvance_verifyTestDoesNotHang() {
301 arrangeMediaPlayer()
302
303 audioPlayerController.play()
304 testCoroutineDispatchers.advanceTimeBy(500) // Play part of the audio track before pausing.
305 audioPlayerController.pause()
306 testCoroutineDispatchers.advanceTimeBy(2000)
307
308 verify(mockAudioPlayerObserver, atLeastOnce()).onChanged(audioPlayerResultCaptor.capture())
309 assertThat(audioPlayerResultCaptor.value.getOrThrow().type).isEqualTo(PlayStatus.PAUSED)
310 // Verify: If the test does not hang, the behavior is correct.
311 }
312
313 @Test
314 fun testScheduling_preparePlayer_invokeCompletionAndAdvance_verifyTestDoesNotHang() {
315 arrangeMediaPlayer()
316
317 audioPlayerController.play()
318 testCoroutineDispatchers.advanceTimeBy(2000)
319 shadowMediaPlayer.invokeCompletionListener()
320 testCoroutineDispatchers.advanceTimeBy(2000)
321
322 verify(mockAudioPlayerObserver, atLeastOnce()).onChanged(audioPlayerResultCaptor.capture())
323 assertThat(audioPlayerResultCaptor.value.getOrThrow().type).isEqualTo(PlayStatus.COMPLETED)
324 // Verify: If the test does not hang, the behavior is correct.
325 }
326
327 @Test
328 fun testScheduling_observeData_removeObserver_verifyTestDoesNotHang() {
329 val playProgress = audioPlayerController.initializeMediaPlayer()
330 audioPlayerController.changeDataSource(TEST_URL)
331 testCoroutineDispatchers.runCurrent()
332
333 playProgress.observeForever(mockAudioPlayerObserver)
334 audioPlayerController.play()
335 testCoroutineDispatchers.advanceTimeBy(2000)
336 playProgress.removeObserver(mockAudioPlayerObserver)
337
338 // Verify: If the test does not hang, the behavior is correct.
339 }
340
341 @Test
342 fun testScheduling_addAndRemoveObservers_verifyTestDoesNotHang() {
343 val playProgress =
344 audioPlayerController.initializeMediaPlayer()
345 audioPlayerController.changeDataSource(TEST_URL)
346 testCoroutineDispatchers.runCurrent()
347
348 audioPlayerController.play()
349 testCoroutineDispatchers.advanceTimeBy(2000)
350 playProgress.observeForever(mockAudioPlayerObserver)
351 audioPlayerController.pause()
352 playProgress.removeObserver(mockAudioPlayerObserver)
353 audioPlayerController.play()
354
355 // Verify: If the test does not hang, the behavior is correct.
356 }
357
358 @Test
359 fun testController_invokeErrorListener_invokePrepared_verifyAudioStatusIsFailure() {
360 audioPlayerController.initializeMediaPlayer().observeForever(mockAudioPlayerObserver)
361 audioPlayerController.changeDataSource(TEST_URL)
362
363 shadowMediaPlayer.invokeErrorListener(/* what= */ 0, /* extra= */ 0)
364 shadowMediaPlayer.invokePreparedListener()
365
366 verify(mockAudioPlayerObserver, atLeastOnce()).onChanged(audioPlayerResultCaptor.capture())
367 assertThat(audioPlayerResultCaptor.value.isFailure()).isTrue()
368 }
369
370 @Test
371 fun testController_notInitialized_releasePlayer_fails() {
372 val exception = assertThrows(IllegalStateException::class) {
373 audioPlayerController.releaseMediaPlayer()
374 }
375
376 assertThat(exception).hasMessageThat()
377 .contains("Media player has not been previously initialized")
378 }
379
380 @Test
381 fun testError_notPrepared_invokePlay_fails() {
382 val exception = assertThrows(IllegalStateException::class) {
383 audioPlayerController.play()
384 }
385
386 assertThat(exception).hasMessageThat().contains("Media Player not in a prepared state")
387 }
388
389 @Test
390 fun testError_notPrepared_invokePause_fails() {
391 val exception = assertThrows(IllegalStateException::class) {
392 audioPlayerController.pause()
393 }
394
395 assertThat(exception).hasMessageThat().contains("Media Player not in a prepared state")
396 }
397
398 @Test
399 fun testError_notPrepared_invokeSeekTo_fails() {
400 val exception = assertThrows(IllegalStateException::class) {
401 audioPlayerController.seekTo(500)
402 }
403
404 assertThat(exception).hasMessageThat().contains("Media Player not in a prepared state")
405 }
406
407 @Test
408 fun testController_initializePlayer_invokePrepared_reportsfailure_logsException() {
409 audioPlayerController.initializeMediaPlayer()
410 audioPlayerController.changeDataSource(TEST_FAIL_URL)
411
412 shadowMediaPlayer.invokePreparedListener()
413 val exception = fakeExceptionLogger.getMostRecentException()
414
415 assertThat(exception).isInstanceOf(IOException::class.java)
416 assertThat(exception).hasMessageThat().contains("Invalid URL")
417 }
418
419 private fun arrangeMediaPlayer() {
420 audioPlayerController.initializeMediaPlayer().observeForever(mockAudioPlayerObserver)
421 audioPlayerController.changeDataSource(TEST_URL)
422 shadowMediaPlayer.invokePreparedListener()
423 testCoroutineDispatchers.runCurrent()
424 }
425
426 private fun addMediaInfo() {
427 val dataSource = DataSource.toDataSource(context, Uri.parse(TEST_URL))
428 val dataSource2 = DataSource.toDataSource(context, Uri.parse(TEST_URL2))
429 val dataSource3 = DataSource.toDataSource(context, Uri.parse(TEST_FAIL_URL))
430 val mediaInfo = ShadowMediaPlayer.MediaInfo(
431 /* duration= */ 2000,
432 /* preparationDelay= */ 0
433 )
434 ShadowMediaPlayer.addMediaInfo(dataSource, mediaInfo)
435 ShadowMediaPlayer.addMediaInfo(dataSource2, mediaInfo)
436 ShadowMediaPlayer.addException(dataSource3, IOException("Invalid URL"))
437 }
438
439 // TODO(#89): Move to a common test library.
440 private fun <T : Throwable> assertThrows(type: KClass<T>, operation: () -> Unit): T {
441 try {
442 operation()
443 fail("Expected to encounter exception of $type")
444 } catch (t: Throwable) {
445 if (type.isInstance(t)) {
446 return type.cast(t)
447 }
448 // Unexpected exception; throw it.
449 throw t
450 }
451 }
452
453 private fun AsyncResult<PlayProgress>.hasStatus(playStatus: PlayStatus): Boolean {
454 return isCompleted() && getOrThrow().type == playStatus
455 }
456
457 private fun setUpTestApplicationComponent() {
458 DaggerAudioPlayerControllerTest_TestApplicationComponent.builder()
459 .setApplication(ApplicationProvider.getApplicationContext())
460 .build()
461 .inject(this)
462 }
463
464 // TODO(#89): Move this to a common test application component.
465 @Module
466 class TestModule {
467 @Provides
468 @Singleton
469 fun provideContext(application: Application): Context {
470 return application
471 }
472
473 // TODO(#59): Either isolate these to their own shared test module, or use the real logging
474 // module in tests to avoid needing to specify these settings for tests.
475 @EnableConsoleLog
476 @Provides
477 fun provideEnableConsoleLog(): Boolean = true
478
479 @EnableFileLog
480 @Provides
481 fun provideEnableFileLog(): Boolean = false
482
483 @GlobalLogLevel
484 @Provides
485 fun provideGlobalLogLevel(): LogLevel = LogLevel.VERBOSE
486
487 @CacheAssetsLocally
488 @Provides
489 fun provideCacheAssetsLocally(): Boolean = false
490 }
491
492 // TODO(#89): Move this to a common test application component.
493 @Singleton
494 @Component(
495 modules = [
496 TestModule::class, TestLogReportingModule::class, LogStorageModule::class,
497 TestDispatcherModule::class
498 ]
499 )
500 interface TestApplicationComponent {
501 @Component.Builder
502 interface Builder {
503 @BindsInstance
504 fun setApplication(application: Application): Builder
505 fun build(): TestApplicationComponent
506 }
507
508 fun inject(audioPlayerControllerTest: AudioPlayerControllerTest)
509 }
510 }
generated on 2021-02-01 23:16
@peculiaruc
Copy link
Author

peculiaruc commented Feb 1, 2021

Code coverage result of Android studio:
Screenshot from 2021-02-01 23-27-50
Screenshot from 2021-02-01 23-33-19

@peculiaruc
Copy link
Author

peculiaruc commented Feb 1, 2021

Code coverage result of Jacoco:

![Screenshot from 2021-01-16 15-40-51](https://user-images.github usercontent.com/35475543/106527187-296a7e00-64e7-11eb-9135-65a50375b623.png)

@anandwana001
Copy link

Looks good to me. Few points:

  1. If we are doing any change here in file AudioPlayerControllerTest, include this file in PR so that we can understand what changes we are doing here in order to fix code coverage for this file.
  2. Jacoco is giving 100% code coverage but the android studio is giving 96% line code coverage. Can we confirm that this jacoco result is the line code coverage or the method code coverage or it is just a file code coverage that a file has been covered and because of that it is giving 100% result?
  3. As from my comment here oppia/oppia-android#2466 (comment) in point 1 the result from the android studio and jacoco are different as I am seeing above comment, than which code coverage we should follow? Also, it looks like the android studio is giving more detailed result but can we do line code coverage using jacoco?

@peculiaruc
Copy link
Author

peculiaruc commented Feb 4, 2021

Looks good to me. Few points:

1. If we are doing any change here in file `AudioPlayerControllerTest`, include this file in PR so that we can understand what changes we are doing here in order to fix code coverage for this file.

2. Jacoco is giving 100% code coverage but the android studio is giving 96% line code coverage. Can we confirm that this jacoco result is the line code coverage or the method code coverage or it is just a file code coverage that a file has been covered and because of that it is giving 100% result?

3. As from my comment here [oppia/oppia-android#2466 (comment)](https://github.com/oppia/oppia-android/pull/2466#issuecomment-768055765) in point 1 the result from the android studio and jacoco are different as I am seeing above comment, than which code coverage we should follow? Also, it looks like the android studio is giving more detailed result but can we do line code coverage using `jacoco`?

For 1: (a) Noted
For 2: (b) I think jacoco is reporting line/method code coverage. But with Jacoco, the total status shows 100% with green colour if they passed, Then it specified the line/method code coverage with a yellow colour when it is ignored but the status shows 100%, If it fails, it will specify the line/method code coverage with red color but the percentage covered changed
For 3: (c) Yes android studio is giving more details. But i am also seeing that jacoco provides details in it own way as seen in No (a) above, But i am trying to find out why the 100% in Jacoco and Android 96% seeing that all test passed.

@peculiaruc
Copy link
Author

peculiaruc commented Feb 7, 2021

Sample for ignored test for No a) above
image

Sample for failed test for No a) above
image

image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment