Skip to content

Commit

Permalink
Add describe based Robolectric test
Browse files Browse the repository at this point in the history
  • Loading branch information
takahirom committed Jun 25, 2024
1 parent 97e65e1 commit a0a295d
Show file tree
Hide file tree
Showing 2 changed files with 216 additions and 96 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package io.github.droidkaigi.confsched.testing

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

fun <T> DescribedTestCase<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.Check -> {
if (step.description == targetCheckDescription) {
step.action(robot)
}
}

is TestNode.Describe -> {}
}
println("Step executed: $index")
}
}

sealed class TestNode<T> {
data class Describe<T>(val description: String, val children: List<TestNode<T>>) : TestNode<T>()
data class Run<T>(val action: (T) -> Unit) : TestNode<T>()
data class Check<T>(val description: String, val action: (T) -> Unit) : TestNode<T>()
}

data class DescribedTestCase<T>(
val description: String,
val steps: List<TestNode<T>>,
val targetCheckDescription: String,
) {
override fun toString(): String = description
}

data class AncestryNode<T>(
val node: TestNode<T>,
val childIndex: Int,
)

data class CheckNode<T>(
val description: String,
val fullDescription: String,
val node: TestNode.Check<T>,
val ancestry: List<AncestryNode<T>>,
)

class TestCaseTreeBuilder<T> {
private val children = mutableListOf<TestNode<T>>()

fun describe(description: String, block: TestCaseTreeBuilder<T>.() -> Unit) {
val builder = TestCaseTreeBuilder<T>()
builder.block()
children.add(TestNode.Describe(description, builder.children))
}

fun run(action: (T) -> Unit) {
children.add(TestNode.Run { robot -> action(robot as T) })
}

fun check(description: String, action: (T) -> Unit) {
children.add(TestNode.Check(description) { robot -> action(robot as T) })
}

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

fun <T> generateTestCases(root: TestNode.Describe<T>): List<DescribedTestCase<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>>()

fun traverse(node: TestNode<T>, parentDescription: String, ancestry: List<AncestryNode<T>>) {
when (node) {
is TestNode.Describe -> {
val currentDescription =
if (parentDescription.isEmpty()) node.description else "$parentDescription - ${node.description}"
node.children.forEachIndexed { index, child ->
val currentAncestry = ancestry + AncestryNode(node, index)
traverse(child, currentDescription, currentAncestry)
}
}

is TestNode.Check -> {
val fullDescription = if (parentDescription.isNotBlank()) {
"$parentDescription-${node.description}"
} else {
node.description
}
checkNodes.add(CheckNode(node.description, fullDescription, node, ancestry))
}

is TestNode.Run -> {}
}
}

traverse(root, "", emptyList())
return checkNodes
}

/**
* 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> {
val steps = mutableListOf<TestNode<T>>()

fun processNode(node: TestNode<T>, ancestry: List<TestNode<T>>, depth: Int) {
when (node) {
is TestNode.Describe -> {
for (child in node.children) {
if (depth + 1 < checkNode.ancestry.size && child == checkNode.ancestry[depth + 1].node) {
processNode(child, ancestry + node, depth + 1)
} else if (child is TestNode.Run) {
steps.add(child)
} else if (child == checkNode.node) {
steps.add(child)
}
}
}

is TestNode.Run -> {
steps.add(node)
}

is TestNode.Check -> {
if (node == checkNode.node) {
steps.add(node)
}
}
}
}

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

return DescribedTestCase(checkNode.fullDescription, steps, checkNode.description)
}

Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
package io.github.droidkaigi.confsched.sessions

import androidx.test.ext.junit.runners.AndroidJUnit4
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.RobotTestRule
import io.github.droidkaigi.confsched.testing.TimetableServerRobot.ServerStatus
import io.github.droidkaigi.confsched.testing.describeTests
import io.github.droidkaigi.confsched.testing.execute
import io.github.droidkaigi.confsched.testing.robot.TimetableScreenRobot
import io.github.droidkaigi.confsched.testing.runRobot
import io.github.droidkaigi.confsched.testing.todoChecks
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.Config
import org.robolectric.ParameterizedRobolectricTestRunner
import javax.inject.Inject

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

@get:Rule
@BindValue val robotTestRule: RobotTestRule = RobotTestRule(this)
Expand All @@ -25,101 +26,69 @@ class TimetableScreenTest {
lateinit var timetableScreenRobot: TimetableScreenRobot

@Test
fun checkLaunchShot() {
fun runTest() {
runRobot(timetableScreenRobot) {
setupTimetableScreenContent()
captureScreenWithChecks(checks = todoChecks("TODO: Please add some checks!"))
testCase.execute(timetableScreenRobot)
}
}

@Test
fun checkLaunchServerErrorShot() {
runRobot(timetableScreenRobot) {
setupTimetableServer(ServerStatus.Error)
setupTimetableScreenContent()
captureScreenWithChecks(checks = todoChecks("TODO: Please add some checks!"))
}
}

@Test
fun checkLaunch() {
runRobot(timetableScreenRobot) {
setupTimetableScreenContent()
checkTimetableItemsDisplayed()
}
}

@Test
fun checkLaunchAccessibilityShot() {
runRobot(timetableScreenRobot) {
setupTimetableScreenContent()
checkAccessibilityCapture()
}
}

@Test
fun checkBookmarkToggleShot() {
runRobot(timetableScreenRobot) {
setupTimetableScreenContent()
clickFirstSessionBookmark()
captureScreenWithChecks()
clickFirstSessionBookmark()
captureScreenWithChecks()
}
}

@Test
fun checkScrollShot() {
runRobot(timetableScreenRobot) {
setupTimetableScreenContent()
scrollTimetable()
captureScreenWithChecks()
}
}

@Test
fun checkGridShot() {
runRobot(timetableScreenRobot) {
setupTimetableScreenContent()
clickTimetableUiTypeChangeButton()
captureScreenWithChecks()
}
}

@Test
fun checkGridScrollShot() {
runRobot(timetableScreenRobot) {
setupTimetableScreenContent()
clickTimetableUiTypeChangeButton()
scrollTimetable()
captureScreenWithChecks()
}
}

@Test
@Config(fontScale = 0.5f)
fun checkSmallFontScaleShot() {
runRobot(timetableScreenRobot) {
setupTimetableScreenContent()
captureScreenWithChecks(checks = todoChecks("TODO: Please add some checks!"))
}
}

@Test
@Config(fontScale = 1.5f)
fun checkLargeFontScaleShot() {
runRobot(timetableScreenRobot) {
setupTimetableScreenContent()
captureScreenWithChecks(checks = todoChecks("TODO: Please add some checks!"))
}
}

@Test
@Config(fontScale = 2.0f)
fun checkHugeFontScaleShot() {
runRobot(timetableScreenRobot) {
setupTimetableScreenContent()
captureScreenWithChecks(checks = todoChecks("TODO: Please add some checks!"))
companion object {
@JvmStatic
@ParameterizedRobolectricTestRunner.Parameters(name = "{0}")
fun tests(): List<DescribedTestCase<TimetableScreenRobot>> {
return describeTests<TimetableScreenRobot> {
describe("TimetableScreenTest") {
describe("when server is operational") {
run { robot ->
robot.setupTimetableServer(ServerStatus.Operational)
robot.setupTimetableScreenContent()
}
check("should show timetable items") { robot ->
robot.captureScreenWithChecks(checks = {
robot.checkTimetableItemsDisplayed()
})
}
describe("click first session bookmark") {
run { robot ->
robot.clickFirstSessionBookmark()
}
check("should show bookmarked session") { robot ->
// FIXME: Add check for bookmarked session
robot.captureScreenWithChecks()
}
}
describe("click first session") {
run { robot ->
robot.clickFirstSession()
}
check("should show session detail") { robot ->
// FIXME: Add check for navigation to session detail
robot.captureScreenWithChecks()
}
}
describe("click timetable ui type change button") {
run { robot ->
robot.clickTimetableUiTypeChangeButton()
}
check("should change timetable ui type") { robot ->
// FIXME: Add check for timetable ui type change
robot.captureScreenWithChecks()
}
}
}
describe("when server is down") {
run { robot ->
robot.setupTimetableServer(ServerStatus.Error)
robot.setupTimetableScreenContent()
}
check("should show error message") { robot ->
// FIXME: Add check for error message
robot.captureScreenWithChecks()
}
}
}
}
}
}
}

0 comments on commit a0a295d

Please sign in to comment.