-
Notifications
You must be signed in to change notification settings - Fork 200
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #70 from DroidKaigi/takahirom/add-describe-tests-b…
…ased-on-describe-spec/2024-06-25 Add describe based Robolectric test
- Loading branch information
Showing
4 changed files
with
222 additions
and
101 deletions.
There are no files selected for viewing
150 changes: 150 additions & 0 deletions
150
...in/java/io/github/droidkaigi/confsched/testing/RobolectricDescribeSpecParameterBuilder.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
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 { action() }) | ||
} | ||
|
||
fun check(description: String, action: T.() -> Unit) { | ||
children.add(TestNode.Check(description) { action() }) | ||
} | ||
|
||
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters