Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions SaidIt/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ dependencies {
testImplementation(libs.mockito.core)
testImplementation(libs.mockito.kotlin)
testImplementation(libs.robolectric)
testRuntimeOnly(libs.robolectric.android.all)
testImplementation(libs.coroutines.test)
// Hilt testing for Robolectric/JUnit
testImplementation(libs.hilt.android.testing)
Expand Down
5 changes: 5 additions & 0 deletions SaidIt/src/test/kotlin/eu/mrogalski/android/ViewsTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,17 @@ import android.widget.TextView
import org.junit.Assert.*
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.*
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config

/**
* Comprehensive unit tests for Views utility functions.
* Tests both legacy Java-compatible methods and modern Kotlin extensions.
*/
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [34])
class ViewsTest {

private lateinit var rootViewGroup: ViewGroup
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment
import org.robolectric.annotation.Config
import org.robolectric.shadows.ShadowApplication
import org.robolectric.Shadows
import org.robolectric.shadows.ShadowNotificationManager

/**
Expand Down Expand Up @@ -78,7 +79,7 @@ class NotifyFileReceiverTest {
receiver.onSuccess(testUri)

// Then
val shadowNotificationManager = ShadowNotificationManager.shadowOf(
val shadowNotificationManager = Shadows.shadowOf(
context.getSystemService(Context.NOTIFICATION_SERVICE) as android.app.NotificationManager
)
val notifications = shadowNotificationManager.allNotifications
Expand All @@ -97,7 +98,7 @@ class NotifyFileReceiverTest {
receiver.onSuccess(testUri)

// Then
val shadowNotificationManager = ShadowNotificationManager.shadowOf(
val shadowNotificationManager = Shadows.shadowOf(
context.getSystemService(Context.NOTIFICATION_SERVICE) as android.app.NotificationManager
)
val notifications = shadowNotificationManager.allNotifications
Expand All @@ -117,7 +118,7 @@ class NotifyFileReceiverTest {
receiver.onSuccess(testUri)

// Then
val shadowNotificationManager = ShadowNotificationManager.shadowOf(
val shadowNotificationManager = Shadows.shadowOf(
context.getSystemService(Context.NOTIFICATION_SERVICE) as android.app.NotificationManager
)
val notification = shadowNotificationManager.getNotification(43)
Expand All @@ -134,7 +135,7 @@ class NotifyFileReceiverTest {
receiver.onFailure(exception)

// Then - no exception thrown, no notification posted
val shadowNotificationManager = ShadowNotificationManager.shadowOf(
val shadowNotificationManager = Shadows.shadowOf(
context.getSystemService(Context.NOTIFICATION_SERVICE) as android.app.NotificationManager
)
val notifications = shadowNotificationManager.allNotifications
Expand Down Expand Up @@ -224,15 +225,15 @@ class NotifyFileReceiverTest {

// Test without permission
receiver.onSuccess(testUri)
var shadowNotificationManager = ShadowNotificationManager.shadowOf(
var shadowNotificationManager = Shadows.shadowOf(
context.getSystemService(Context.NOTIFICATION_SERVICE) as android.app.NotificationManager
)
assertEquals(0, shadowNotificationManager.allNotifications.size)

// Grant permission and test again
ShadowApplication.getInstance().grantPermissions(Manifest.permission.POST_NOTIFICATIONS)
receiver.onSuccess(testUri)
shadowNotificationManager = ShadowNotificationManager.shadowOf(
shadowNotificationManager = Shadows.shadowOf(
context.getSystemService(Context.NOTIFICATION_SERVICE) as android.app.NotificationManager
)
assertEquals(1, shadowNotificationManager.allNotifications.size)
Expand Down Expand Up @@ -279,7 +280,7 @@ class NotifyFileReceiverTest {
receiver.onSuccess(testUri2)

// Then - only one notification because same ID is used
val shadowNotificationManager = ShadowNotificationManager.shadowOf(
val shadowNotificationManager = Shadows.shadowOf(
context.getSystemService(Context.NOTIFICATION_SERVICE) as android.app.NotificationManager
)
val notification = shadowNotificationManager.getNotification(43)
Expand Down
17 changes: 10 additions & 7 deletions SaidIt/src/test/kotlin/eu/mrogalski/saidit/SaidItFragmentTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import com.google.android.material.button.MaterialButtonToggleGroup
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.textview.MaterialTextView
import eu.mrogalski.android.TimeFormat
import eu.mrogalski.saidit.NotifyFileReceiver
import eu.mrogalski.saidit.PromptFileReceiver
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
Expand All @@ -26,6 +28,7 @@ import org.mockito.junit.MockitoJUnitRunner
import org.mockito.MockedStatic
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment
import org.robolectric.Shadows.shadowOf
import org.robolectric.annotation.Config

/**
Expand Down Expand Up @@ -245,7 +248,7 @@ class SaidItFragmentTest {
val fileName = "test_recording.mp4"

// When
val notification = fragment.buildNotificationForFile(appContext, fileUri, fileName)
val notification = NotifyFileReceiver.buildNotificationForFile(appContext, fileUri, fileName)

// Then
assert(notification != null)
Expand All @@ -257,10 +260,10 @@ class SaidItFragmentTest {
// Given - Use Robolectric's application context
val appContext = RuntimeEnvironment.getApplication()
val mockUri = mock(Uri::class.java)
val receiver = fragment.NotifyFileReceiver(appContext)
val receiver = NotifyFileReceiver(appContext)

// Grant POST_NOTIFICATIONS permission so NotificationManagerCompat.notify() is reachable
org.robolectric.Shadows.shadowOf(appContext).grantPermissions(android.Manifest.permission.POST_NOTIFICATIONS)
shadowOf(appContext).grantPermissions(android.Manifest.permission.POST_NOTIFICATIONS)

// Mock static method using Mockito
mockStatic(NotificationManagerCompat::class.java).use { mockedStatic ->
Expand All @@ -278,7 +281,7 @@ class SaidItFragmentTest {
@Test
fun `NotifyFileReceiver onFailure does nothing`() {
// Given
val receiver = fragment.NotifyFileReceiver(mockContext)
val receiver = NotifyFileReceiver(mockContext)
val exception = Exception("Test failure")

// When
Expand All @@ -296,7 +299,7 @@ class SaidItFragmentTest {
`when`(mockActivity.isFinishing).thenReturn(false)
`when`(mockDialog.isShowing).thenReturn(true)

val receiver = fragment.PromptFileReceiver(mockActivity, mockDialog)
val receiver = PromptFileReceiver(mockActivity, mockDialog)

// Mock the dialog builder chain
val mockBuilder = mock(MaterialAlertDialogBuilder::class.java)
Expand All @@ -320,7 +323,7 @@ class SaidItFragmentTest {
val mockUri = mock(Uri::class.java)
`when`(mockActivity.isFinishing).thenReturn(true)

val receiver = fragment.PromptFileReceiver(mockActivity)
val receiver = PromptFileReceiver(mockActivity)

// When
receiver.onSuccess(mockUri)
Expand All @@ -335,7 +338,7 @@ class SaidItFragmentTest {
val exception = Exception("Test error")
`when`(mockActivity.isFinishing).thenReturn(false)

val receiver = fragment.PromptFileReceiver(mockActivity)
val receiver = PromptFileReceiver(mockActivity)

// Mock the dialog builder
val mockBuilder = mock(MaterialAlertDialogBuilder::class.java)
Expand Down
3 changes: 1 addition & 2 deletions audio/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,7 @@ dependencies {
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("com.google.android.material:material:1.8.0")
testImplementation("junit:junit:4.13.2")
testImplementation("org.robolectric:robolectric:4.11.1")
testImplementation("androidx.test:core:1.5.0")
testImplementation("io.mockk:mockk:1.13.13")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,21 @@ package com.siya.epistemophile.audio
import android.media.MediaPlayer
import java.io.File

class AudioPlayer(private val inputFile: File) {
class AudioPlayer(
private val inputFile: File,
private val playerProvider: () -> MediaPlayer = { MediaPlayer() }
) {

private var player: MediaPlayer? = null

fun start() {
try {
player = MediaPlayer().apply {
val instance = playerProvider().apply {
setDataSource(inputFile.absolutePath)
prepare()
start()
}
player = instance
} catch (e: Exception) {
e.printStackTrace()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,23 @@ package com.siya.epistemophile.audio
import android.media.MediaRecorder
import java.io.File

class AudioRecorder(private val outputFile: File) {
class AudioRecorder(
private val outputFile: File,
private val recorderProvider: () -> MediaRecorder = { MediaRecorder() }
) {

private var recorder: MediaRecorder? = null

fun start() {
recorder = MediaRecorder().apply {
val instance = recorderProvider().apply {
setAudioSource(MediaRecorder.AudioSource.MIC)
setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
setAudioEncoder(MediaRecorder.AudioEncoder.AAC)
setOutputFile(outputFile.absolutePath)
prepare()
start()
}
recorder = instance
}

fun stop() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,49 +1,71 @@
package com.siya.epistemophile.audio

import android.content.Context
import androidx.test.core.app.ApplicationProvider
import android.media.MediaPlayer
import android.media.MediaRecorder
import io.mockk.confirmVerified
import io.mockk.every
import io.mockk.mockk
import io.mockk.verifyOrder
import org.junit.After
import org.junit.Before
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.MethodSorters
import org.robolectric.RobolectricTestRunner
import java.io.File
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit

@RunWith(RobolectricTestRunner::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
class AudioPlayerRecorderTest {

private lateinit var audioRecorder: AudioRecorder
private lateinit var audioPlayer: AudioPlayer
private lateinit var outputFile: File
private lateinit var mediaRecorder: MediaRecorder
private lateinit var mediaPlayer: MediaPlayer

@Before
fun setUp() {
outputFile = kotlin.io.path.createTempFile(prefix = "test_recording", suffix = ".mp4").toFile()
mediaRecorder = mockk(relaxed = true)
mediaPlayer = mockk(relaxed = true)

every { mediaRecorder.setOutputFile(any<String>()) } returns Unit
every { mediaPlayer.setDataSource(any<String>()) } returns Unit
}

@After
fun tearDown() {
outputFile.delete()
}

@Test
fun test1_recordAndPlayback() {
val context = ApplicationProvider.getApplicationContext<Context>()
val outputFile = File(context.cacheDir, "test_recording.mp4")
audioRecorder = AudioRecorder(outputFile)
audioPlayer = AudioPlayer(outputFile)

// Use CountDownLatch for synchronization instead of Thread.sleep()
val recordLatch = CountDownLatch(1)
val playbackLatch = CountDownLatch(1)

audioRecorder.start()
// Let recording run for a short time
Thread.sleep(100)
audioRecorder.stop()
recordLatch.countDown()

audioPlayer.start()
// Let playback run for a short time
Thread.sleep(100)
audioPlayer.stop()
playbackLatch.countDown()

// Verify that the operations completed
recordLatch.await(1, TimeUnit.SECONDS)
playbackLatch.await(1, TimeUnit.SECONDS)
fun `start and stop recorder and player`() {
val recorder = AudioRecorder(outputFile) { mediaRecorder }
val player = AudioPlayer(outputFile) { mediaPlayer }

recorder.start()
verifyOrder {
mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC)
mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC)
mediaRecorder.setOutputFile(outputFile.absolutePath)
mediaRecorder.prepare()
mediaRecorder.start()
}

recorder.stop()
verifyOrder {
mediaRecorder.stop()
mediaRecorder.release()
}
confirmVerified(mediaRecorder)

player.start()
verifyOrder {
mediaPlayer.setDataSource(outputFile.absolutePath)
mediaPlayer.prepare()
mediaPlayer.start()
}

player.stop()
verifyOrder {
mediaPlayer.stop()
mediaPlayer.release()
}
confirmVerified(mediaPlayer)
}
}
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ junit = "4.13.2"
mockito = "5.11.0"
mockito-kotlin = "5.0.0"
robolectric = "4.11.1"
robolectric-android-all = "14-robolectric-10818077"
tap-target-view = "1.13.3"
javax-inject = "1"

Expand All @@ -25,6 +26,7 @@ junit = { module = "junit:junit", version.ref = "junit" }
mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockito" }
mockito-kotlin = { module = "org.mockito.kotlin:mockito-kotlin", version.ref = "mockito-kotlin" }
robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" }
robolectric-android-all = { module = "org.robolectric:android-all", version.ref = "robolectric-android-all" }
coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" }
coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" }
Expand Down
Loading