From 22c50d50f565fa54e244e4160b85006af20af94e Mon Sep 17 00:00:00 2001 From: Niels van Velzen Date: Fri, 11 Feb 2022 22:20:21 +0100 Subject: [PATCH] Rewrite all unit tests using kotest --- app/build.gradle.kts | 7 +- app/src/test/.editorconfig | 8 ++ ...erTest.kt => VideoSpeedControllerTests.kt} | 97 ++++++++----------- .../overlay/CustomSeekProviderTest.kt | 40 -------- .../overlay/CustomSeekProviderTests.kt | 37 +++++++ app/src/test/kotlin/util/DeviceUtilsTest.kt | 90 ----------------- app/src/test/kotlin/util/DeviceUtilsTests.kt | 73 ++++++++++++++ app/src/test/kotlin/util/TimeUtilsTest.kt | 30 ------ app/src/test/kotlin/util/TimeUtilsTests.kt | 28 ++++++ gradle/libs.versions.toml | 6 +- 10 files changed, 199 insertions(+), 217 deletions(-) create mode 100644 app/src/test/.editorconfig rename app/src/test/kotlin/ui/playback/{VideoSpeedControllerTest.kt => VideoSpeedControllerTests.kt} (58%) delete mode 100644 app/src/test/kotlin/ui/playback/overlay/CustomSeekProviderTest.kt create mode 100644 app/src/test/kotlin/ui/playback/overlay/CustomSeekProviderTests.kt delete mode 100644 app/src/test/kotlin/util/DeviceUtilsTest.kt create mode 100644 app/src/test/kotlin/util/DeviceUtilsTests.kt delete mode 100644 app/src/test/kotlin/util/TimeUtilsTest.kt create mode 100644 app/src/test/kotlin/util/TimeUtilsTests.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 5cb7dcafa..38d507152 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -67,6 +67,10 @@ android { sarifReport = true checkDependencies = true } + + testOptions.unitTests.all { + it.useJUnitPlatform() + } } val versionTxt by tasks.registering { @@ -148,6 +152,7 @@ dependencies { coreLibraryDesugaring(libs.android.desugar) // Testing - testImplementation(libs.junit) + testImplementation(libs.kotest.runner.junit5) + testImplementation(libs.kotest.assertions) testImplementation(libs.mockk) } diff --git a/app/src/test/.editorconfig b/app/src/test/.editorconfig new file mode 100644 index 000000000..48863a1ff --- /dev/null +++ b/app/src/test/.editorconfig @@ -0,0 +1,8 @@ +# Note: This file is temporarily, these settings should be moved to the top-level editorconfig to +# affect the complete project. + +[{*.kts,*.kt}] +# Disable wildcard imports in IntelliJ/Android Studio +ij_kotlin_name_count_to_use_star_import = 1000 +ij_kotlin_name_count_to_use_star_import_for_members = 1000 +ij_kotlin_packages_to_use_import_on_demand = diff --git a/app/src/test/kotlin/ui/playback/VideoSpeedControllerTest.kt b/app/src/test/kotlin/ui/playback/VideoSpeedControllerTests.kt similarity index 58% rename from app/src/test/kotlin/ui/playback/VideoSpeedControllerTest.kt rename to app/src/test/kotlin/ui/playback/VideoSpeedControllerTests.kt index aa8e8c9d1..638f104a0 100644 --- a/app/src/test/kotlin/ui/playback/VideoSpeedControllerTest.kt +++ b/app/src/test/kotlin/ui/playback/VideoSpeedControllerTests.kt @@ -1,30 +1,27 @@ package org.jellyfin.androidtv.ui.playback -import io.mockk.* -import org.junit.After -import org.junit.Assert.assertEquals -import org.junit.Test +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.doubles.plusOrMinus +import io.kotest.matchers.shouldBe +import io.mockk.every +import io.mockk.justRun +import io.mockk.mockk +import io.mockk.slot +import io.mockk.verify -class VideoSpeedControllerTest { - @After - fun tearDown() { +class VideoSpeedControllerTests : FunSpec({ + afterTest { // Always reset the "user selected" speed back to default VideoSpeedController(mockk(relaxed = true)).resetSpeedToDefault() } - @Test - fun testSpeedSteps() { - val speedSteps = VideoSpeedController.SpeedSteps.values() - val expectedStep = 0.25 - var i = 1 - speedSteps.forEach { v -> - assertEquals(i * expectedStep, v.speed, 0.001) - i += 1 + test("VideoSpeedController.SpeedSteps uses intervals of 0.25") { + VideoSpeedController.SpeedSteps.values().forEachIndexed { i, v -> + v.speed.shouldBe((i + 1) * 0.25 plusOrMinus 0.0001) } } - @Test - fun testControllerSpeedIsOneByDefault() { + test("VideoSpeedController speed is 1 by default") { val mockController = mockk(relaxed = true) val slot = slot() justRun { mockController.setPlaybackSpeed(capture(slot)) } @@ -32,20 +29,20 @@ class VideoSpeedControllerTest { VideoSpeedController(mockController) verify { mockController.setPlaybackSpeed(any()) } - assertEquals(1.0, slot.captured, 0.001) + slot.captured shouldBe (1.0 plusOrMinus 0.0001) } - @Test - fun testSetNewSpeed() { + test("VideoSpeedController.currentSpeed getter returns set value") { val mockController = mockk(relaxed = true) val controller = VideoSpeedController(mockController) + val expected = VideoSpeedController.SpeedSteps.SPEED_1_25 controller.currentSpeed = expected - assertEquals(expected, controller.currentSpeed) + + controller.currentSpeed shouldBe expected } - @Test - fun testSetNewSpeedSetsOnManager() { + test("VideoSpeedController.currentSpeed updates the speed in the controller") { val mockController = mockk(relaxed = true) val slot = slot() justRun { mockController.setPlaybackSpeed(capture(slot)) } @@ -55,11 +52,10 @@ class VideoSpeedControllerTest { controller.currentSpeed = expected verify { mockController.setPlaybackSpeed(any()) } - assertEquals(expected.speed, slot.captured, 0.0001) + slot.captured shouldBe (expected.speed plusOrMinus 0.0001) } - @Test - fun testResetPreviousSpeedToDefault() { + test("VideoSpeedController.resetSpeedToDefault() works correctly") { val playbackController = mockk(relaxed = true) val videoController = VideoSpeedController(playbackController) @@ -71,20 +67,13 @@ class VideoSpeedControllerTest { VideoSpeedController(playbackController) verify { playbackController.setPlaybackSpeed(any()) } - assertEquals( - VideoSpeedController.SpeedSteps.SPEED_1_00.speed, - slot.captured, - 0.0001 - ) + slot.captured shouldBe (VideoSpeedController.SpeedSteps.SPEED_1_00.speed plusOrMinus 0.0001) } - - @Test - fun testControllerPreservesMostRecentlySelectedSpeedConstructingNew() { + test("VideoSpeedController remembers previous instance speed value") { var lastSetSpeed = 1.0 - val speeds = VideoSpeedController.SpeedSteps.values() - speeds.forEach { newSpeed -> + VideoSpeedController.SpeedSteps.values().forEach { newSpeed -> val mockController = mockk(relaxed = true) val slot = slot() justRun { mockController.setPlaybackSpeed(capture(slot)) } @@ -92,48 +81,48 @@ class VideoSpeedControllerTest { val controller = VideoSpeedController(mockController) verify { mockController.setPlaybackSpeed(any()) } - assertEquals(lastSetSpeed, slot.captured, 0.001) + slot.captured shouldBe (lastSetSpeed plusOrMinus 0.0001) controller.currentSpeed = newSpeed lastSetSpeed = newSpeed.speed } } - @Test - fun testSpeedResetsToOneWithLiveTv() { + test("VideoSpeedController.currentSpeed always sets the speed to 1 for LiveTV") { // Since handling live TV is more complex, we will simply reset the playback // speed to 1 so we can't out-run the current buffer. // Assume the user has pre-set their speed - VideoSpeedController(mockk(relaxed = true)).currentSpeed = - VideoSpeedController.SpeedSteps.SPEED_2_00 + VideoSpeedController(mockk(relaxed = true)).currentSpeed = VideoSpeedController.SpeedSteps.SPEED_2_00 // Then they switch to live-tv - val mockController = mockk(relaxed = true) - every { mockController.isLiveTv } returns true + val mockController = mockk(relaxed = true) { + every { isLiveTv } returns true + } val speedController = VideoSpeedController(mockController) - assertEquals(VideoSpeedController.SpeedSteps.SPEED_1_00, speedController.currentSpeed) verify { mockController.setPlaybackSpeed(1.0) } + speedController.currentSpeed shouldBe VideoSpeedController.SpeedSteps.SPEED_1_00 // Try to set it back to other values should be ignored speedController.currentSpeed = VideoSpeedController.SpeedSteps.SPEED_2_00 - assertEquals(VideoSpeedController.SpeedSteps.SPEED_1_00, speedController.currentSpeed) + verify { mockController.setPlaybackSpeed(1.0) } + speedController.currentSpeed shouldBe VideoSpeedController.SpeedSteps.SPEED_1_00 } - @Test - fun testSpeedChangeableOffLiveTv() { - val mockController = mockk(relaxed = true) - every { mockController.isLiveTv } returns false + test("VideoSpeedController.currentSpeed alwats sets the requested speed when LiveTV is off") { + val mockController = mockk(relaxed = true) { + every { isLiveTv } returns false + } val speedController = VideoSpeedController(mockController) - assertEquals(VideoSpeedController.SpeedSteps.SPEED_1_00, speedController.currentSpeed) verify { mockController.setPlaybackSpeed(1.0) } + speedController.currentSpeed shouldBe VideoSpeedController.SpeedSteps.SPEED_1_00 speedController.currentSpeed = VideoSpeedController.SpeedSteps.SPEED_2_00 - assertEquals(VideoSpeedController.SpeedSteps.SPEED_2_00, speedController.currentSpeed) - verify { mockController.setPlaybackSpeed(2.0) } - } -} + verify { mockController.setPlaybackSpeed(2.0) } + speedController.currentSpeed shouldBe VideoSpeedController.SpeedSteps.SPEED_2_00 + } +}) diff --git a/app/src/test/kotlin/ui/playback/overlay/CustomSeekProviderTest.kt b/app/src/test/kotlin/ui/playback/overlay/CustomSeekProviderTest.kt deleted file mode 100644 index 2811e4985..000000000 --- a/app/src/test/kotlin/ui/playback/overlay/CustomSeekProviderTest.kt +++ /dev/null @@ -1,40 +0,0 @@ -package org.jellyfin.androidtv.ui.playback.overlay - -import io.mockk.every -import io.mockk.mockk -import org.junit.Assert.assertArrayEquals -import org.junit.Assert.assertEquals -import org.junit.Test - -class CustomSeekProviderTest { - @Test - fun testGetSeekPositions_withSimpleDuration() { - val videoPlayerAdapter = mockk() - every { videoPlayerAdapter.canSeek() } returns true - every { videoPlayerAdapter.duration } returns 90000L - val customSeekProvider = CustomSeekProvider(videoPlayerAdapter) - val expected = longArrayOf(0L, 30000L, 60000L, 90000L) - val actual = customSeekProvider.seekPositions - assertArrayEquals(expected, actual) - } - - @Test - fun testGetSeekPositions_withOddDuration() { - val videoPlayerAdapter = mockk() - every { videoPlayerAdapter.canSeek() } returns true - every { videoPlayerAdapter.duration } returns 130000L - val customSeekProvider = CustomSeekProvider(videoPlayerAdapter) - val expected = longArrayOf(0L, 30000, 60000, 90000, 120000, 130000) - val actual = customSeekProvider.seekPositions - assertArrayEquals(expected, actual) - } - - @Test - fun testGetSeekPositions_withSeekDisabled() { - val videoPlayerAdapter = mockk() - every { videoPlayerAdapter.canSeek() } returns false - val customSeekProvider = CustomSeekProvider(videoPlayerAdapter) - val actual = customSeekProvider.seekPositions - assertEquals(0, actual.size.toLong()) - } -} diff --git a/app/src/test/kotlin/ui/playback/overlay/CustomSeekProviderTests.kt b/app/src/test/kotlin/ui/playback/overlay/CustomSeekProviderTests.kt new file mode 100644 index 000000000..5ad7ebf52 --- /dev/null +++ b/app/src/test/kotlin/ui/playback/overlay/CustomSeekProviderTests.kt @@ -0,0 +1,37 @@ +package org.jellyfin.androidtv.ui.playback.overlay + +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe +import io.mockk.every +import io.mockk.mockk + +class CustomSeekProviderTests : FunSpec({ + test("CustomSeekProvider.seekPositions with simple duration") { + val videoPlayerAdapter = mockk { + every { canSeek() } returns true + every { duration } returns 90000L + } + val customSeekProvider = CustomSeekProvider(videoPlayerAdapter) + + customSeekProvider.seekPositions shouldBe arrayOf(0L, 30000L, 60000L, 90000L) + } + + test("CustomSeekProvider.seekPositions with odd duration") { + val videoPlayerAdapter = mockk { + every { canSeek() } returns true + every { duration } returns 130000L + } + val customSeekProvider = CustomSeekProvider(videoPlayerAdapter) + + customSeekProvider.seekPositions shouldBe arrayOf(0L, 30000, 60000, 90000, 120000, 130000) + } + + test("CustomSeekProvider.seekPositions with seek disabled") { + val videoPlayerAdapter = mockk { + every { canSeek() } returns false + } + val customSeekProvider = CustomSeekProvider(videoPlayerAdapter) + + customSeekProvider.seekPositions.size shouldBe 0 + } +}) diff --git a/app/src/test/kotlin/util/DeviceUtilsTest.kt b/app/src/test/kotlin/util/DeviceUtilsTest.kt deleted file mode 100644 index 491e3f36b..000000000 --- a/app/src/test/kotlin/util/DeviceUtilsTest.kt +++ /dev/null @@ -1,90 +0,0 @@ -package org.jellyfin.androidtv.util - -import io.mockk.every -import io.mockk.mockkStatic -import io.mockk.unmockkAll -import org.junit.After -import org.junit.Assert.* -import org.junit.Test - -class DeviceUtilsTest { - @After - fun tearDown() = unmockkAll() - - @Test - fun testBuildModelIsNull() { - // A number of the tests below rely on the assumption - // that Build.MODEL is Unknown in unit tests - assertEquals("Unknown", DeviceUtils.getBuildModel()) - } - - @Test - fun testMethodsCanHandleNullModel() { - // Methods that implicitly rely on Build.MODEL - val methods = arrayOf( - DeviceUtils::isChromecastWithGoogleTV, - DeviceUtils::isFireTv, - DeviceUtils::isFireTvStickGen1, - DeviceUtils::isFireTvStick4k, - DeviceUtils::isShieldTv, - DeviceUtils::has4kVideoSupport - ) - - for (method in methods) { - assertFalse(method.toString(), method()) - } - } - - private fun mockBuildModel(mockedVal: String) { - mockkStatic(DeviceUtils::class) - every { DeviceUtils.getBuildModel() } returns mockedVal - } - - @Test - fun testIsChromecastGoogleTvTrue() { - mockBuildModel("Chromecast") - assertTrue(DeviceUtils.isChromecastWithGoogleTV()) - } - - @Test - fun testIsFireTV() { - val acceptableInputs = arrayOf("AFT", "AFT_foo", "AFT ", "AFT2") - for (input in acceptableInputs) { - mockBuildModel(input) - assertTrue(DeviceUtils.isFireTv()) - } - } - - @Test - fun testIsFireTVGen1() { - mockBuildModel("AFTM") - assertTrue(DeviceUtils.isFireTv()) - assertTrue(DeviceUtils.isFireTvStickGen1()) - assertFalse(DeviceUtils.isFireTvStick4k()) - } - - @Test - fun testIsFireTV4k() { - mockBuildModel("AFTMM") - assertTrue(DeviceUtils.isFireTv()) - assertTrue(DeviceUtils.isFireTvStick4k()) - assertFalse(DeviceUtils.isFireTvStickGen1()) - } - - @Test - fun testIsShieldTv() { - mockBuildModel("SHIELD Android TV") - assertTrue(DeviceUtils.isShieldTv()) - } - - @Test - fun testDisabled4kModels() { - val fire1080pSticks = arrayOf( - "AFTM", "AFTT", "AFTSSS", "AFTSS", "AFTB", "AFTS" - ) - for (input in fire1080pSticks) { - mockBuildModel(input) - assertFalse(input, DeviceUtils.has4kVideoSupport()) - } - } -} diff --git a/app/src/test/kotlin/util/DeviceUtilsTests.kt b/app/src/test/kotlin/util/DeviceUtilsTests.kt new file mode 100644 index 000000000..598f64ac1 --- /dev/null +++ b/app/src/test/kotlin/util/DeviceUtilsTests.kt @@ -0,0 +1,73 @@ +package org.jellyfin.androidtv.util + +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe +import io.mockk.every +import io.mockk.mockkStatic +import io.mockk.unmockkStatic + +class DeviceUtilsTests : FunSpec({ + test("DeviceUtils.getBuildModel() is unknown") { + DeviceUtils.getBuildModel() shouldBe "Unknown" + } + + test("DeviceUtils methods support unknown as model") { + DeviceUtils.isChromecastWithGoogleTV() shouldBe false + DeviceUtils.isFireTv() shouldBe false + DeviceUtils.isFireTvStickGen1() shouldBe false + DeviceUtils.isFireTvStick4k() shouldBe false + DeviceUtils.isShieldTv() shouldBe false + DeviceUtils.has4kVideoSupport() shouldBe false + } + + fun withBuildModel(buildModel: String, block: () -> Unit) { + mockkStatic(DeviceUtils::class) + every { DeviceUtils.getBuildModel() } returns buildModel + block() + unmockkStatic(DeviceUtils::class) + } + + test("DeviceUtils.isChromecastWithGoogleTV() works correctly") { + withBuildModel("Chromecast") { + DeviceUtils.isChromecastWithGoogleTV() shouldBe true + } + } + + test("DeviceUtils.isFireTv() works correctly") { + arrayOf("AFT", "AFT_foo", "AFT ", "AFT2").forEach { input -> + withBuildModel(input) { + DeviceUtils.isFireTv() shouldBe true + } + } + } + + test("DeviceUtils.isFireTvStickGen1() works correctly") { + withBuildModel("AFTM") { + DeviceUtils.isFireTv() shouldBe true + DeviceUtils.isFireTvStickGen1() shouldBe true + DeviceUtils.isFireTvStick4k() shouldBe false + } + } + + test("DeviceUtils.isFireTvStick4k() works correctly") { + withBuildModel("AFTMM") { + DeviceUtils.isFireTv() shouldBe true + DeviceUtils.isFireTvStick4k() shouldBe true + DeviceUtils.isFireTvStickGen1() shouldBe false + } + } + + test("DeviceUtils.isShieldTv() works correctly") { + withBuildModel("SHIELD Android TV") { + DeviceUtils.isShieldTv() shouldBe true + } + } + + test("DeviceUtils.has4kVideoSupport() works correctly") { + arrayOf("AFTM", "AFTT", "AFTSSS", "AFTSS", "AFTB", "AFTS").forEach { input -> + withBuildModel(input) { + DeviceUtils.has4kVideoSupport() shouldBe false + } + } + } +}) diff --git a/app/src/test/kotlin/util/TimeUtilsTest.kt b/app/src/test/kotlin/util/TimeUtilsTest.kt deleted file mode 100644 index f41933756..000000000 --- a/app/src/test/kotlin/util/TimeUtilsTest.kt +++ /dev/null @@ -1,30 +0,0 @@ -package org.jellyfin.androidtv.util - -import org.junit.Assert.assertEquals -import org.junit.Test - -class TimeUtilsTest { - @Test - fun secondsToMillis() { - assertEquals(0, TimeUtils.secondsToMillis(0.0)) - assertEquals(1000, TimeUtils.secondsToMillis(1.0)) - assertEquals(1250, TimeUtils.secondsToMillis(1.25)) - } - - @Test - fun formatMillis() { - assertEquals("0:00", TimeUtils.formatMillis(0)) - assertEquals("0:13", TimeUtils.formatMillis(13000)) - assertEquals("5:00", TimeUtils.formatMillis(300000)) - assertEquals("9:01", TimeUtils.formatMillis(541000)) - assertEquals("9:59", TimeUtils.formatMillis(599000)) - assertEquals("26:00", TimeUtils.formatMillis(1560000)) - assertEquals("26:01", TimeUtils.formatMillis(1561000)) - assertEquals("26:43", TimeUtils.formatMillis(1603000)) - assertEquals("1:00:00", TimeUtils.formatMillis(3600000)) - assertEquals("1:01:01", TimeUtils.formatMillis(3661000)) - assertEquals("1:09:15", TimeUtils.formatMillis(4155000)) - assertEquals("1:16:03", TimeUtils.formatMillis(4563489)) - assertEquals("12:00:00", TimeUtils.formatMillis(43200000)) - } -} diff --git a/app/src/test/kotlin/util/TimeUtilsTests.kt b/app/src/test/kotlin/util/TimeUtilsTests.kt new file mode 100644 index 000000000..f6d5c0d20 --- /dev/null +++ b/app/src/test/kotlin/util/TimeUtilsTests.kt @@ -0,0 +1,28 @@ +package org.jellyfin.androidtv.util + +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe + +class TimeUtilsTests : FunSpec({ + test("TimeUtils.secondstoMillis() works correctly") { + TimeUtils.secondsToMillis(0.0) shouldBe 0 + TimeUtils.secondsToMillis(1.0) shouldBe 1000 + TimeUtils.secondsToMillis(1.25) shouldBe 1250 + } + + test("TimeUtils.formatMillis() works correctly") { + TimeUtils.formatMillis(0) shouldBe "0:00" + TimeUtils.formatMillis(13000) shouldBe "0:13" + TimeUtils.formatMillis(300000) shouldBe "5:00" + TimeUtils.formatMillis(541000) shouldBe "9:01" + TimeUtils.formatMillis(599000) shouldBe "9:59" + TimeUtils.formatMillis(1560000) shouldBe "26:00" + TimeUtils.formatMillis(1561000) shouldBe "26:01" + TimeUtils.formatMillis(1603000) shouldBe "26:43" + TimeUtils.formatMillis(3600000) shouldBe "1:00:00" + TimeUtils.formatMillis(3661000) shouldBe "1:01:01" + TimeUtils.formatMillis(4155000) shouldBe "1:09:15" + TimeUtils.formatMillis(4563489) shouldBe "1:16:03" + TimeUtils.formatMillis(43200000) shouldBe "12:00:00" + } +}) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 59a858fa8..c18553ac2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -35,6 +35,7 @@ libvlc = "3.4.7" mockk = "1.12.1" slf4j-timber = "3.1" timber = "5.0.1" +kotest = "5.1.0" [plugins] aboutlibraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "aboutlibraries" } @@ -109,8 +110,9 @@ leakcanary = { module = "com.squareup.leakcanary:leakcanary-android", version.re # Compatibility (desugaring) android-desugar = { module = "com.android.tools:desugar_jdk_libs", version.ref = "android-desugar" } -# Testing -junit = { module = "junit:junit", version.ref = "junit" } +# Test utilities +kotest-runner-junit5 = { module = "io.kotest:kotest-runner-junit5", version.ref = "kotest" } +kotest-assertions = { module = "io.kotest:kotest-assertions-core", version.ref = "kotest" } mockk = { module = "io.mockk:mockk", version.ref = "mockk" } [bundles]