Skip to content

Instantly share code, notes, and snippets.

@AniFichadia
Last active May 6, 2025 15:57
Show Gist options
  • Save AniFichadia/0a0aa665dfc554e963d2ab3276247efb to your computer and use it in GitHub Desktop.
Save AniFichadia/0a0aa665dfc554e963d2ab3276247efb to your computer and use it in GitHub Desktop.
JUnit classes that skip subsequent tests if any test in a test suite fails
import org.junit.internal.AssumptionViolatedException
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement
/**
* @author Aniruddh Fichadia
* @date 2020-04-03
*/
class SkipOnFirstFailureTestRule(
var enabled: Boolean = false
) : TestRule {
override fun apply(base: Statement, description: Description): Statement = object : Statement() {
@Throws(Throwable::class)
override fun evaluate() {
onTestStarted(description)
try {
base.evaluate()
} catch (e: AssumptionViolatedException) {
// Referring to org.junit.rules.TestWatcher, AssumptionViolatedException's are thrown when a test has been skipped / ignored
} catch (e: Throwable) {
onTestFailed(e, description)
// Propagate the original exception
throw e
}
}
}
@Throws(AssumptionViolatedException::class)
private fun onTestStarted(description: Description) {
if (enabled) {
failedTestName?.let { failedTestName ->
// Imitate a test skip if there's been a previously failed test
throw AssumptionViolatedException("$failedTestName is the first test to fail, skipping ${getTestName(description)}")
}
}
}
private fun onTestFailed(e: Throwable, description: Description) {
if (enabled && failedTestName == null) {
failedTestName = getTestName(description)
}
}
private fun getTestName(description: Description) = "${description.className}.${description.methodName}"
private companion object {
// Store in a companion object to allow persistence across test classes. This is a bit lazy, but it works.
var failedTestName: String? = null
}
}
import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner
import org.junit.runner.Description
import org.junit.runner.Runner
import org.junit.runner.manipulation.Filter
import org.junit.runner.manipulation.Filterable
import org.junit.runner.manipulation.Sortable
import org.junit.runner.manipulation.Sorter
import org.junit.runner.notification.Failure
import org.junit.runner.notification.RunListener
import org.junit.runner.notification.RunNotifier
import org.junit.runners.model.FrameworkMethod
/**
* @author Aniruddh Fichadia
* @date 2020-04-03
*/
class SkipOnFirstFailureTestRunner(klass: Class<*>) : Runner(), Filterable, Sortable {
private val enabled: Boolean = true
// Note: Currently this does not support RobolectricTestRunner, but a similar check may be added as AndroidJUnit4.loadRunner(...)
private val delegateRunner = SkipOnFirstFailureAndroidJUnit4ClassRunner(klass)
override fun run(notifier: RunNotifier) {
if (enabled) {
notifier.addListener(object : RunListener() {
override fun testFailure(failure: Failure) {
super.testFailure(failure)
// Listen for test failures
testHasFailed = true
}
})
}
delegateRunner.run(notifier)
}
override fun getDescription(): Description = delegateRunner.description
override fun filter(filter: Filter) = (delegateRunner as Filterable).filter(filter)
override fun sort(sorter: Sorter) = (delegateRunner as Sortable).sort(sorter)
private class SkipOnFirstFailureAndroidJUnit4ClassRunner(klass: Class<*>) : AndroidJUnit4ClassRunner(klass) {
override fun runChild(method: FrameworkMethod, notifier: RunNotifier) {
if (!testHasFailed) {
super.runChild(method, notifier)
} else {
notifier.fireTestIgnored(describeChild(method))
}
}
}
private companion object {
// Store in a companion object to allow persistence across test classes. This is a bit lazy, but it works.
var testHasFailed: Boolean = false
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment