JUnit5:如何重复失败的测试?

dzieciou:

其中的实践中,许多企业遵循的是重复不稳定的测试,直到经过X次(连续或全部)。如果执行ñ次,失败至少通过X倍失败它被标记。

TestNG的支撑与以下注释:

@Test(invocationCount = 5, successPercentage = 40)

如何实现与JUnit5类似的功能?

有一个在JUnit5类似的标注,称之为@RepeatedTest(5)但没有条件执行。

mkobit:

好吧,我花了时间去鞭一点点一起如何做到这一点用一个小例子TestTemplateInvocationContextProviderExecutionConditionTestExecutionExceptionHandler扩展点。

我能够处理失败的测试的办法是将它们标记为“胎死腹中”,而不是让他们平了失败(从而使整个测试执行不认为这是一个失败的),只有失败的测试时,我们无法获得最低达到成功的奔跑。如果测试的最低金额已经成功,那么我们也标志着余下的测试为“已禁用”。测试失败是在跟踪ExtensionContext.Store,使国家能够在每个位置上进行查找。

这是一个非常粗略的例子,肯定有一些问题,但希望能充当怎样构图不同的注解的例子。我结束了在科特林写它:

@Retry-esque注释松散的基础上TestNG的例子:

import org.junit.jupiter.api.TestTemplate
import org.junit.jupiter.api.extension.ExtendWith

@TestTemplate
@Target(AnnotationTarget.FUNCTION)
@ExtendWith(RetryTestExtension::class)
annotation class Retry(val invocationCount: Int, val minSuccess: Int)

TestTemplateInvocationContext 使用模板化测试:

import org.junit.jupiter.api.extension.Extension
import org.junit.jupiter.api.extension.TestTemplateInvocationContext

class RetryTemplateContext(
  private val invocation: Int,
  private val maxInvocations: Int,
  private val minSuccess: Int
) : TestTemplateInvocationContext {
  override fun getDisplayName(invocationIndex: Int): String {
    return "Invocation number $invocationIndex (requires $minSuccess success)"
  }

  override fun getAdditionalExtensions(): MutableList<Extension> {
    return mutableListOf(
      RetryingTestExecutionExtension(invocation, maxInvocations, minSuccess)
    )
  }
}

TestTemplateInvocationContextProvider扩展的@Retry注释:

import org.junit.jupiter.api.extension.ExtensionContext
import org.junit.jupiter.api.extension.ExtensionContextException
import org.junit.jupiter.api.extension.TestTemplateInvocationContext
import org.junit.jupiter.api.extension.TestTemplateInvocationContextProvider
import org.junit.platform.commons.support.AnnotationSupport
import java.util.stream.IntStream
import java.util.stream.Stream

class RetryTestExtension : TestTemplateInvocationContextProvider {
  override fun supportsTestTemplate(context: ExtensionContext): Boolean {
    return context.testMethod.map { it.isAnnotationPresent(Retry::class.java) }.orElse(false)
  }

  override fun provideTestTemplateInvocationContexts(context: ExtensionContext): Stream<TestTemplateInvocationContext> {
    val annotation = AnnotationSupport.findAnnotation(
        context.testMethod.orElseThrow { ExtensionContextException("Must be annotated on method") },
        Retry::class.java
    ).orElseThrow { ExtensionContextException("${Retry::class.java} not found on method") }

    checkValidRetry(annotation)

    return IntStream.rangeClosed(1, annotation.invocationCount)
        .mapToObj { RetryTemplateContext(it, annotation.invocationCount, annotation.minSuccess) }
  }

  private fun checkValidRetry(annotation: Retry) {
    if (annotation.invocationCount < 1) {
      throw ExtensionContextException("${annotation.invocationCount} must be greater than or equal to 1")
    }
    if (annotation.minSuccess < 1 || annotation.minSuccess > annotation.invocationCount) {
      throw ExtensionContextException("Invalid ${annotation.minSuccess}")
    }
  }
}

简单data class表示重试(注入测试用例在使用本例ParameterResolver)。

data class RetryInfo(val invocation: Int, val maxInvocations: Int)

Exception 用来表示失败重试:

import java.lang.Exception

internal class RetryingTestFailure(invocation: Int, cause: Throwable) : Exception("Failed test execution at invocation #$invocation", cause)

主要推广实施ExecutionConditionParameterResolver以及TestExecutionExceptionHandler

import org.junit.jupiter.api.extension.ConditionEvaluationResult
import org.junit.jupiter.api.extension.ExecutionCondition
import org.junit.jupiter.api.extension.ExtensionContext
import org.junit.jupiter.api.extension.ParameterContext
import org.junit.jupiter.api.extension.ParameterResolver
import org.junit.jupiter.api.extension.TestExecutionExceptionHandler
import org.opentest4j.TestAbortedException

internal class RetryingTestExecutionExtension(
  private val invocation: Int,
  private val maxInvocations: Int,
  private val minSuccess: Int
) : ExecutionCondition, ParameterResolver, TestExecutionExceptionHandler {
  override fun evaluateExecutionCondition(
    context: ExtensionContext
  ): ConditionEvaluationResult {
    val failureCount = getFailures(context).size
    // Shift -1 because this happens before test
    val successCount = (invocation - 1) - failureCount
    when {
      (maxInvocations - failureCount) < minSuccess -> // Case when we cannot hit our minimum success
        return ConditionEvaluationResult.disabled("Cannot hit minimum success rate of $minSuccess/$maxInvocations - $failureCount failures already")
      successCount < minSuccess -> // Case when we haven't hit success threshold yet
        return ConditionEvaluationResult.enabled("Have not ran $minSuccess/$maxInvocations successful executions")
      else -> return ConditionEvaluationResult.disabled("$minSuccess/$maxInvocations successful runs have already ran. Skipping run $invocation")
    }
  }

  override fun supportsParameter(
    parameterContext: ParameterContext,
    extensionContext: ExtensionContext
  ): Boolean = parameterContext.parameter.type == RetryInfo::class.java

  override fun resolveParameter(
    parameterContext: ParameterContext,
    extensionContext: ExtensionContext
  ): Any = RetryInfo(invocation, maxInvocations)

  override fun handleTestExecutionException(
    context: ExtensionContext,
    throwable: Throwable
  ) {

    val testFailure = RetryingTestFailure(invocation, throwable)
    val failures: MutableList<RetryingTestFailure> = getFailures(context)
    failures.add(testFailure)
    val failureCount = failures.size
    val successCount = invocation - failureCount
    if ((maxInvocations - failureCount) < minSuccess) {
      throw testFailure
    } else if (successCount < minSuccess) {
      // Case when we have still have retries left
      throw TestAbortedException("Aborting test #$invocation/$maxInvocations- still have retries left",
        testFailure)
    }
  }

  private fun getFailures(context: ExtensionContext): MutableList<RetryingTestFailure> {
    val namespace = ExtensionContext.Namespace.create(
      RetryingTestExecutionExtension::class.java)
    val store = context.parent.get().getStore(namespace)
    @Suppress("UNCHECKED_CAST")
    return store.getOrComputeIfAbsent(context.requiredTestMethod.name, { mutableListOf<RetryingTestFailure>() }, MutableList::class.java) as MutableList<RetryingTestFailure>
  }
}

然后,测试消费者:

import org.junit.jupiter.api.DisplayName

internal class MyRetryableTest {
  @DisplayName("Fail all retries")
  @Retry(invocationCount = 5, minSuccess = 3)
  internal fun failAllRetries(retryInfo: RetryInfo) {
    println(retryInfo)
    throw Exception("Failed at $retryInfo")
  }

  @DisplayName("Only fail once")
  @Retry(invocationCount = 5, minSuccess = 4)
  internal fun succeedOnRetry(retryInfo: RetryInfo) {
    if (retryInfo.invocation == 1) {
      throw Exception("Failed at ${retryInfo.invocation}")
    }
  }

  @DisplayName("Only requires single success and is first execution")
  @Retry(invocationCount = 5, minSuccess = 1)
  internal fun firstSuccess(retryInfo: RetryInfo) {
    println("Running: $retryInfo")
  }

  @DisplayName("Only requires single success and is last execution")
  @Retry(invocationCount = 5, minSuccess = 1)
  internal fun lastSuccess(retryInfo: RetryInfo) {
    if (retryInfo.invocation < 5) {
      throw Exception("Failed at ${retryInfo.invocation}")
    }
  }

  @DisplayName("All required all succeed")
  @Retry(invocationCount = 5, minSuccess = 5)
  internal fun allRequiredAllSucceed(retryInfo: RetryInfo) {
    println("Running: $retryInfo")
  }

  @DisplayName("Fail early and disable")
  @Retry(invocationCount = 5, minSuccess = 4)
  internal fun failEarly(retryInfo: RetryInfo) {
    throw Exception("Failed at ${retryInfo.invocation}")
  }
}

而在的IntelliJ测试输出的样子:

的IntelliJ测试输出

我不知道扔TestAbortedExceptionTestExecutionExceptionHandler.handleTestExecutionException应该终止测试,但我在这里使用它。

本文收集自互联网,转载请注明来源。

如有侵权,请联系 [email protected] 删除。

编辑于
0

我来说两句

0 条评论
登录 后参与评论

相关文章