Skip to content

Commit

Permalink
Merge pull request #100 from DroidKaigi/takahirom/refactor-test-namin…
Browse files Browse the repository at this point in the history
…g/2024-07-07

Refactor to use BDD idea
  • Loading branch information
takahirom authored Jul 7, 2024
2 parents 451b3ce + c6a0c3e commit 6ce7e2e
Show file tree
Hide file tree
Showing 6 changed files with 77 additions and 53 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.github.droidkaigi.confsched.testing

import androidx.compose.ui.test.junit4.ComposeTestRule
import com.github.takahirom.roborazzi.roboOutputName
import io.github.droidkaigi.confsched.data.contributors.ContributorsApiClient
import io.github.droidkaigi.confsched.data.contributors.FakeContributorsApiClient
import io.github.droidkaigi.confsched.data.eventmap.EventMapApiClient
Expand Down Expand Up @@ -66,6 +67,13 @@ fun todoChecks(@Suppress("UNUSED_PARAMETER") reason: String): () -> Unit {
class DefaultCaptureScreenRobot @Inject constructor(private val robotTestRule: RobotTestRule) :
CaptureScreenRobot {
override fun captureScreenWithChecks(checks: () -> Unit) {
val roboOutputName = roboOutputName()
if (roboOutputName.contains("[") && roboOutputName.contains("]")) {
val name = roboOutputName.substringAfter("[").substringBefore("]")
robotTestRule.captureScreen(name)
checks()
return
}
robotTestRule.captureScreen()
checks()
}
Expand All @@ -75,12 +83,13 @@ class DefaultCaptureScreenRobot @Inject constructor(private val robotTestRule: R
replaceWith = ReplaceWith("captureScreenWithChecks(checks)"),
)
override fun captureScreenWithChecks() {
robotTestRule.captureScreen()
captureScreenWithChecks { }
}
}

interface WaitRobot {
fun waitUntilIdle()
fun wait5Seconds()
}

class DefaultWaitRobot @Inject constructor(
Expand All @@ -94,6 +103,14 @@ class DefaultWaitRobot @Inject constructor(
ShadowLooper.runUiThreadTasksIncludingDelayedTasks()
}
}

override fun wait5Seconds() {
repeat(5) {
testDispatcher.scheduler.advanceTimeBy(1.seconds)
robotTestRule.composeTestRule.mainClock.advanceTimeBy(1000)
ShadowLooper.runUiThreadTasksIncludingDelayedTasks()
}
}
}

interface FontScaleRobot {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
package io.github.droidkaigi.confsched.testing

inline fun <reified T> describeTests(block: TestCaseTreeBuilder<T>.() -> Unit): List<DescribedTestCase<T>> {
inline fun <reified T> describeBehaviors(
name: String,
block: TestCaseTreeBuilder<T>.() -> Unit,
): List<DescribedBehavior<T>> {
val builder = TestCaseTreeBuilder<T>()
builder.block()
val root = builder.build()
return generateTestCases(root)
val root = builder.build(name = name)
return generateBehaviors(root)
}

suspend fun <T> DescribedTestCase<T>.execute(robot: T) {
suspend fun <T> DescribedBehavior<T>.execute(robot: T) {
for ((index, step) in steps.withIndex()) {
println("Executing step: $index ($description)")
when (step) {
is TestNode.Run -> step.action(robot)
is TestNode.It -> {
is TestNode.ItShould -> {
if (step.description == targetCheckDescription) {
step.action(robot)
}
Expand All @@ -27,10 +30,10 @@ suspend fun <T> DescribedTestCase<T>.execute(robot: T) {
sealed class TestNode<T> {
data class Describe<T>(val description: String, val children: List<TestNode<T>>) : TestNode<T>()
data class Run<T>(val action: suspend T.() -> Unit) : TestNode<T>()
data class It<T>(val description: String, val action: suspend T.() -> Unit) : TestNode<T>()
data class ItShould<T>(val description: String, val action: suspend T.() -> Unit) : TestNode<T>()
}

data class DescribedTestCase<T>(
data class DescribedBehavior<T>(
val description: String,
val steps: List<TestNode<T>>,
val targetCheckDescription: String,
Expand All @@ -46,7 +49,7 @@ data class AncestryNode<T>(
data class CheckNode<T>(
val description: String,
val fullDescription: String,
val node: TestNode.It<T>,
val node: TestNode.ItShould<T>,
val ancestry: List<AncestryNode<T>>,
)

Expand All @@ -63,21 +66,20 @@ class TestCaseTreeBuilder<T> {
children.add(TestNode.Run { action() })
}

fun it(description: String, action: suspend T.() -> Unit) {
children.add(TestNode.It(description) { action() })
fun itShould(description: String, action: suspend T.() -> Unit) {
children.add(TestNode.ItShould(description) { action() })
}

fun build(): TestNode.Describe<T> = TestNode.Describe("", children)
fun build(name: String): TestNode.Describe<T> = TestNode.Describe(name, children)
}

fun <T> generateTestCases(root: TestNode.Describe<T>): List<DescribedTestCase<T>> {
fun <T> generateBehaviors(root: TestNode.Describe<T>): List<DescribedBehavior<T>> {
val checkNodes = collectCheckNodes(root)
return checkNodes.map { createTestCase(it) }
}

/**
* Collect all check nodes from the test tree
* it will be O(N)
*/
private fun <T> collectCheckNodes(root: TestNode.Describe<T>): List<CheckNode<T>> {
val checkNodes = mutableListOf<CheckNode<T>>()
Expand All @@ -93,9 +95,9 @@ private fun <T> collectCheckNodes(root: TestNode.Describe<T>): List<CheckNode<T>
}
}

is TestNode.It -> {
is TestNode.ItShould -> {
val fullDescription = if (parentDescription.isNotBlank()) {
"$parentDescription - it ${node.description}"
"$parentDescription - it should ${node.description}"
} else {
node.description
}
Expand All @@ -112,10 +114,8 @@ private fun <T> collectCheckNodes(root: TestNode.Describe<T>): List<CheckNode<T>

/**
* Create a test case from a check node
* We only run the steps that are necessary to reach the check node
* so the time complexity might be O(logN)
*/
private fun <T> createTestCase(checkNode: CheckNode<T>): DescribedTestCase<T> {
private fun <T> createTestCase(checkNode: CheckNode<T>): DescribedBehavior<T> {
val steps = mutableListOf<TestNode<T>>()

fun processNode(node: TestNode<T>, ancestry: List<TestNode<T>>, depth: Int) {
Expand All @@ -136,7 +136,7 @@ private fun <T> createTestCase(checkNode: CheckNode<T>): DescribedTestCase<T> {
steps.add(node)
}

is TestNode.It -> {
is TestNode.ItShould -> {
if (node == checkNode.node) {
steps.add(node)
}
Expand All @@ -146,5 +146,5 @@ private fun <T> createTestCase(checkNode: CheckNode<T>): DescribedTestCase<T> {

processNode(checkNode.ancestry.first().node, emptyList(), 0)

return DescribedTestCase(checkNode.fullDescription, steps, checkNode.description)
return DescribedBehavior(checkNode.fullDescription, steps, checkNode.description)
}
Original file line number Diff line number Diff line change
Expand Up @@ -119,9 +119,10 @@ class RobotTestRule(
val engine = FakeImageLoaderEngine.Builder()
.default(ColorDrawable(android.graphics.Color.BLUE))
.build()
val imageLoader = ImageLoader.Builder(ApplicationProvider.getApplicationContext())
.components { add(engine) }
.build()
val imageLoader =
ImageLoader.Builder(ApplicationProvider.getApplicationContext())
.components { add(engine) }
.build()
SingletonImageLoader.setUnsafe(imageLoader)
}
})
Expand Down Expand Up @@ -165,8 +166,12 @@ class RobotTestRule(
}
}

fun captureScreen() {
captureScreenRoboImage()
fun captureScreen(name: String? = null) {
if (name != null) {
captureScreenRoboImage("$name.png")
} else {
captureScreenRoboImage()
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ package io.github.droidkaigi.confsched.sessions
import android.os.Bundle
import dagger.hilt.android.testing.BindValue
import dagger.hilt.android.testing.HiltAndroidTest
import io.github.droidkaigi.confsched.testing.DescribedTestCase
import io.github.droidkaigi.confsched.testing.DescribedBehavior
import io.github.droidkaigi.confsched.testing.RobotTestRule
import io.github.droidkaigi.confsched.testing.TimetableServerRobot.ServerStatus
import io.github.droidkaigi.confsched.testing.describeTests
import io.github.droidkaigi.confsched.testing.describeBehaviors
import io.github.droidkaigi.confsched.testing.execute
import io.github.droidkaigi.confsched.testing.robot.TimetableItemDetailScreenRobot
import io.github.droidkaigi.confsched.testing.runRobot
Expand All @@ -18,7 +18,7 @@ import javax.inject.Inject

@RunWith(ParameterizedRobolectricTestRunner::class)
@HiltAndroidTest
class TimetableItemDetailScreenTest(private val testCase: DescribedTestCase<TimetableItemDetailScreenRobot>) {
class TimetableItemDetailScreenTest(private val testCase: DescribedBehavior<TimetableItemDetailScreenRobot>) {

@get:Rule
@BindValue val robotTestRule: RobotTestRule = RobotTestRule(
Expand All @@ -35,7 +35,7 @@ class TimetableItemDetailScreenTest(private val testCase: DescribedTestCase<Time
lateinit var timetableItemDetailScreenRobot: TimetableItemDetailScreenRobot

@Test
fun checkLaunchShot() {
fun runTest() {
runRobot(timetableItemDetailScreenRobot) {
testCase.execute(timetableItemDetailScreenRobot)
}
Expand All @@ -44,8 +44,8 @@ class TimetableItemDetailScreenTest(private val testCase: DescribedTestCase<Time
companion object {
@JvmStatic
@ParameterizedRobolectricTestRunner.Parameters(name = "{0}")
fun testCases(): List<DescribedTestCase<TimetableItemDetailScreenRobot>> {
return describeTests<TimetableItemDetailScreenRobot> {
fun behaviors(): List<DescribedBehavior<TimetableItemDetailScreenRobot>> {
return describeBehaviors<TimetableItemDetailScreenRobot>(name = "TimetableItemDetailScreen") {
describe("when server is operational") {
run {
setupTimetableServer(ServerStatus.Operational)
Expand All @@ -54,26 +54,27 @@ class TimetableItemDetailScreenTest(private val testCase: DescribedTestCase<Time
run {
setupScreenContent()
}
it("should show session detail title") {
itShould("show session detail title") {
// FIXME: Add check for session detail title
captureScreenWithChecks()
}
it("check accessibility") {
itShould("be appropriately accessible") {
checkAccessibilityCapture()
}
describe("click bookmark button") {
describe("click bookmark") {
run {
clickBookmarkButton()
}
it("should show bookmarked session") {
itShould("show bookmarked session") {
// FIXME: Add check for bookmarked session
captureScreenWithChecks()
}
describe("click bookmark button again") {
describe("click bookmark again") {
run {
clickBookmarkButton()
}
it("should show unbookmarked session") {
itShould("show unbookmarked session") {
wait5Seconds()
// FIXME: Add check for unbookmarked session
captureScreenWithChecks()
}
Expand All @@ -83,7 +84,7 @@ class TimetableItemDetailScreenTest(private val testCase: DescribedTestCase<Time
run {
scroll()
}
it("should show scrolled session detail") {
itShould("show scrolled session detail") {
// FIXME: Add check for scrolled session detail
captureScreenWithChecks()
}
Expand All @@ -94,7 +95,7 @@ class TimetableItemDetailScreenTest(private val testCase: DescribedTestCase<Time
setFontScale(0.5f)
setupScreenContent()
}
it("should show session detail with small font scale") {
itShould("show small font session detail") {
captureScreenWithChecks()
}
}
Expand All @@ -103,7 +104,7 @@ class TimetableItemDetailScreenTest(private val testCase: DescribedTestCase<Time
setFontScale(1.5f)
setupScreenContent()
}
it("should show session detail with large font scale") {
itShould("show small font session detail") {
captureScreenWithChecks()
}
}
Expand All @@ -112,7 +113,7 @@ class TimetableItemDetailScreenTest(private val testCase: DescribedTestCase<Time
setFontScale(2.0f)
setupScreenContent()
}
it("should show session detail with huge font scale") {
itShould("show small font session detail") {
captureScreenWithChecks()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ package io.github.droidkaigi.confsched.sessions

import dagger.hilt.android.testing.BindValue
import dagger.hilt.android.testing.HiltAndroidTest
import io.github.droidkaigi.confsched.testing.DescribedTestCase
import io.github.droidkaigi.confsched.testing.DescribedBehavior
import io.github.droidkaigi.confsched.testing.RobotTestRule
import io.github.droidkaigi.confsched.testing.TimetableServerRobot.ServerStatus
import io.github.droidkaigi.confsched.testing.describeTests
import io.github.droidkaigi.confsched.testing.describeBehaviors
import io.github.droidkaigi.confsched.testing.execute
import io.github.droidkaigi.confsched.testing.robot.TimetableScreenRobot
import io.github.droidkaigi.confsched.testing.runRobot
Expand All @@ -17,7 +17,7 @@ import javax.inject.Inject

@RunWith(ParameterizedRobolectricTestRunner::class)
@HiltAndroidTest
class TimetableScreenTest(private val testCase: DescribedTestCase<TimetableScreenRobot>) {
class TimetableScreenTest(private val testCase: DescribedBehavior<TimetableScreenRobot>) {

@get:Rule
@BindValue val robotTestRule: RobotTestRule = RobotTestRule(this)
Expand All @@ -35,14 +35,14 @@ class TimetableScreenTest(private val testCase: DescribedTestCase<TimetableScree
companion object {
@JvmStatic
@ParameterizedRobolectricTestRunner.Parameters(name = "{0}")
fun testCases(): List<DescribedTestCase<TimetableScreenRobot>> {
return describeTests<TimetableScreenRobot> {
fun behaviors(): List<DescribedBehavior<TimetableScreenRobot>> {
return describeBehaviors<TimetableScreenRobot>(name = "TimetableScreen") {
describe("when server is operational") {
run {
setupTimetableServer(ServerStatus.Operational)
setupTimetableScreenContent()
}
it("should show timetable items") {
itShould("show timetable items") {
captureScreenWithChecks(checks = {
checkTimetableItemsDisplayed()
})
Expand All @@ -51,7 +51,7 @@ class TimetableScreenTest(private val testCase: DescribedTestCase<TimetableScree
run {
clickFirstSessionBookmark()
}
it("should show bookmarked session") {
itShould("show bookmarked session") {
// FIXME: Add check for bookmarked session
captureScreenWithChecks()
}
Expand All @@ -60,15 +60,15 @@ class TimetableScreenTest(private val testCase: DescribedTestCase<TimetableScree
run {
clickFirstSession()
}
it("should show session detail") {
itShould("show session detail") {
checkClickedItemsExists()
}
}
describe("click timetable ui type change button") {
describe("click timetable ui type change") {
run {
clickTimetableUiTypeChangeButton()
}
it("should change timetable ui type") {
itShould("change timetable ui type") {
// FIXME: Add check for timetable ui type change
captureScreenWithChecks()
}
Expand All @@ -79,7 +79,7 @@ class TimetableScreenTest(private val testCase: DescribedTestCase<TimetableScree
setupTimetableServer(ServerStatus.Error)
setupTimetableScreenContent()
}
it("should show error message") {
itShould("show error message") {
// FIXME: Add check for error message
captureScreenWithChecks()
}
Expand Down
1 change: 1 addition & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,4 @@ kotlin.incremental.native=true
# roborazzi
roborazzi.test.record=true
roborazzi.record.namingStrategy=testClassAndMethod
roborazzi.record.filePathStrategy=relativePathFromRoborazziContextOutputDirectory

0 comments on commit 6ce7e2e

Please sign in to comment.