SpecFlow - 重试失败的测试

问题描述 投票:0回答:4

有没有办法实现

AfterScenario
钩子以在失败的情况下重新运行当前测试?

类似这样的:

[AfterScenario("retry")]
public void Retry()
{
    if (ScenarioContext.Current.TestError != null)
    {
     // ?     
    }
}

注意:我项目中的测试合并在有序测试中,并通过MsTest执行。

c# testing hook specflow scenarios
4个回答
6
投票

这个插件太棒了。 https://github.com/arrty/specflow-retry。我让它与 nunit 一起工作,他的例子是使用 MS-Test

它将允许您执行此操作:

@retry:2
Scenario: Tag on scenario is preferred
Then scenario should be run 3 times

5
投票

首先让我说我同意测试应该稳定并且永远不应该重试。然而,我们并不生活在一个理想的世界中,在某些非常具体的场景中,重试测试可能是一个有效的用例。

我正在运行 UI 测试(对角度应用程序使用 selenium),有时 chromedriver 由于不清楚的原因而变得无响应。这种行为完全超出我的控制范围,并且不存在可行的解决方案。我无法在 SpecFlow 步骤中重试此操作,因为我有登录应用程序的“给定”步骤。当它在“何时”步骤中失败时,我还需要重新运行“给定”步骤。在这种情况下,我想关闭驱动程序,再次启动它,然后重新运行之前的所有步骤。作为最后的手段,我为 SpecFlow 编写了一个自定义测试运行程序,可以从如下错误中恢复:

免责声明:这不是预期用途,并且可能会在任何版本的 SpecFlow 中出现问题。如果您是测试纯粹主义者,请不要继续阅读。

首先我们创建一个类,可以轻松创建自定义 ITestRunner(将所有方法提供为虚拟方法,以便可以覆盖它们):

public class OverrideableTestRunner : ITestRunner
{
    private readonly ITestRunner _runner;

    public OverrideableTestRunner(ITestRunner runner)
    {
        _runner = runner;
    }

    public int ThreadId => _runner.ThreadId;

    public FeatureContext FeatureContext => _runner.FeatureContext;

    public ScenarioContext ScenarioContext => _runner.ScenarioContext;

    public virtual void And(string text, string multilineTextArg, Table tableArg, string keyword = null)
    {
        _runner.And(text, multilineTextArg, tableArg, keyword);
    }

    public virtual void But(string text, string multilineTextArg, Table tableArg, string keyword = null)
    {
        _runner.But(text, multilineTextArg, tableArg, keyword);
    }

    public virtual void CollectScenarioErrors()
    {
        _runner.CollectScenarioErrors();
    }

    public virtual void Given(string text, string multilineTextArg, Table tableArg, string keyword = null)
    {
        _runner.Given(text, multilineTextArg, tableArg, keyword);
    }

    public virtual void InitializeTestRunner(int threadId)
    {
        _runner.InitializeTestRunner(threadId);
    }

    public virtual void OnFeatureEnd()
    {
        _runner.OnFeatureEnd();
    }

    public virtual void OnFeatureStart(FeatureInfo featureInfo)
    {
        _runner.OnFeatureStart(featureInfo);
    }

    public virtual void OnScenarioEnd()
    {
        _runner.OnScenarioEnd();
    }

    public virtual void OnScenarioInitialize(ScenarioInfo scenarioInfo)
    {
        _runner.OnScenarioInitialize(scenarioInfo);
    }

    public virtual void OnScenarioStart()
    {
        _runner.OnScenarioStart();
    }

    public virtual void OnTestRunEnd()
    {
        _runner.OnTestRunEnd();
    }

    public virtual void OnTestRunStart()
    {
        _runner.OnTestRunStart();
    }

    public virtual void Pending()
    {
        _runner.Pending();
    }

    public virtual void SkipScenario()
    {
        _runner.SkipScenario();
    }

    public virtual void Then(string text, string multilineTextArg, Table tableArg, string keyword = null)
    {
        _runner.Then(text, multilineTextArg, tableArg, keyword);
    }

    public virtual void When(string text, string multilineTextArg, Table tableArg, string keyword = null)
    {
        _runner.When(text, multilineTextArg, tableArg, keyword);
    }
}

接下来,我们创建自定义测试运行程序,它会记住对场景进行的调用,并可以重新运行前面的步骤:

public class RetryTestRunner : OverrideableTestRunner
{
    /// <summary>
    /// Which exceptions to handle (default: all)
    /// </summary>
    public Predicate<Exception> HandleExceptionFilter { private get; set; } = _ => true;

    /// <summary>
    /// The action that is executed to recover
    /// </summary>
    public Action RecoverAction { private get; set; } = () => { };

    /// <summary>
    /// The maximum number of retries
    /// </summary>
    public int MaxRetries { private get; set; } = 10;

    /// <summary>
    /// The executed actions for this scenario, these need to be replayed in the case of an error
    /// </summary>
    private readonly List<(MethodInfo method, object[] args)> _previousSteps = new List<(MethodInfo method, object[] args)>();

    /// <summary>
    /// The number of the current try (to make sure we don't go over the specified limit)
    /// </summary>
    private int _currentTryNumber = 0;

    public NonSuckingTestRunner(ITestExecutionEngine engine) : base(new TestRunner(engine))
    {
    }

    public override void OnScenarioStart()
    {
        base.OnScenarioStart();

        _previousSteps.Clear();
        _currentTryNumber = 0;
    }

    public override void Given(string text, string multilineTextArg, Table tableArg, string keyword = null)
    {
        base.Given(text, multilineTextArg, tableArg, keyword);
        Checker()(text, multilineTextArg, tableArg, keyword);
    }

    public override void But(string text, string multilineTextArg, Table tableArg, string keyword = null)
    {
        base.But(text, multilineTextArg, tableArg, keyword);
        Checker()(text, multilineTextArg, tableArg, keyword);
    }

    public override void And(string text, string multilineTextArg, Table tableArg, string keyword = null)
    {
        base.And(text, multilineTextArg, tableArg, keyword);
        Checker()(text, multilineTextArg, tableArg, keyword);
    }

    public override void Then(string text, string multilineTextArg, Table tableArg, string keyword = null)
    {
        base.Then(text, multilineTextArg, tableArg, keyword);
        Checker()(text, multilineTextArg, tableArg, keyword);
    }

    public override void When(string text, string multilineTextArg, Table tableArg, string keyword = null)
    {
        base.When(text, multilineTextArg, tableArg, keyword);
        Checker()(text, multilineTextArg, tableArg, keyword);
    }

    // Use this delegate combination to make a params call possible
    // It is not possible to use a params argument and the CallerMemberName
    // in one method, so we curry the method to make it possible. #functionalprogramming
    public delegate void ParamsFunc(params object[] args);

    private ParamsFunc Checker([CallerMemberName] string method = null)
    {
        return args =>
        {
            // Record the previous step
            _previousSteps.Add((GetType().GetMethod(method), args));

            // Determine if we should retry
            if (ScenarioContext.ScenarioExecutionStatus != ScenarioExecutionStatus.TestError || !HandleExceptionFilter(ScenarioContext.TestError) || _currentTryNumber >= MaxRetries)
            {
                return;
            }

            // HACKY: Reset the test state to a non-error state
            typeof(ScenarioContext).GetProperty(nameof(ScenarioContext.ScenarioExecutionStatus)).SetValue(ScenarioContext, ScenarioExecutionStatus.OK);
            typeof(ScenarioContext).GetProperty(nameof(ScenarioContext.TestError)).SetValue(ScenarioContext, null);

            // Trigger the recovery action
            RecoverAction.Invoke();

            // Retry the steps
            _currentTryNumber++;
            var stepsToPlay = _previousSteps.ToList();
            _previousSteps.Clear();
            stepsToPlay.ForEach(s => s.method.Invoke(this, s.args));
        };
    }
}

接下来,配置 SpecFlow 以使用我们自己的测试运行器(这也可以作为插件添加)。

 /// <summary>
/// We need this because this is the only way to configure specflow before it starts
/// </summary>
[TestClass]
public class CustomDependencyProvider : DefaultDependencyProvider
{
    [AssemblyInitialize]
    public static void AssemblyInitialize(TestContext testContext)
    {
        // Override the dependency provider of specflow
        ContainerBuilder.DefaultDependencyProvider = new CustomDependencyProvider();
        TestRunnerManager.OnTestRunStart(typeof(CustomDependencyProvider).Assembly);
    }

    [AssemblyCleanup]
    public static void AssemblyCleanup()
    {
        TestRunnerManager.OnTestRunEnd(typeof(CustomDependencyProvider).Assembly);
    }

    public override void RegisterTestThreadContainerDefaults(ObjectContainer testThreadContainer)
    {
        base.RegisterTestThreadContainerDefaults(testThreadContainer);

        // Use our own testrunner
        testThreadContainer.RegisterTypeAs<NonSuckingTestRunner, ITestRunner>();
    }
}

另外,将其添加到您的 .csproj:

<PropertyGroup>
  <GenerateSpecFlowAssemblyHooksFile>false</GenerateSpecFlowAssemblyHooksFile>
</PropertyGroup>

现在我们可以使用测试运行程序从错误中恢复:

[Binding]
public class TestInitialize
{
    private readonly RetryTestRunner _testRunner;

    public TestInitialize(ITestRunner testRunner)
    {
        _testRunner = testRunner as RetryTestRunner;
    }

    [BeforeScenario()]
    public void TestInit()
    {
        _testRunner.RecoverAction = () =>
        {
            StopDriver();
            StartDriver();
        };

        _testRunner.HandleExceptionFilter = ex => ex is WebDriverException;
    }
}

要在 AfterScenario 步骤中使用此方法,您可以向测试运行程序添加 RetryScenario() 方法并调用该方法。

最后一点:当您无能为力时,将此作为最后的手段。运行片状测试比根本不运行测试要好。


1
投票

我希望能够重试失败的测试,但仍然在测试结果中将其报告为失败。这可以让我轻松识别代码工作的场景,但也容易因网络延迟等原因出现偶发性问题。这些故障的优先级与由于代码更改而导致的新故障的优先级不同。

我设法使用 MsTest 来完成此操作,因为您可以创建一个继承自 TestMethodAttribute 的类。

首先,我将此部分添加到 csproj 文件的底部,以便在生成 *.feature.cs 文件之后但实际构建之前调用自定义 powershell 脚本:

<Target Name="OverrideTestMethodAttribute" BeforeTargets="PrepareForBuild">
    <Message Text="Calling OverrideTestMethodAttribute.ps1" Importance="high" />
    <Exec Command="powershell -Command &quot;$(ProjectDir)OverrideTestMethodAttribute.ps1&quot;" />
</Target>
然后,OverrideTestMethodAttribute.ps1 powershell 脚本执行查找/替换,以更改对我的 IntegrationTestMethodAttribute 的所有 TestMethodAttribute 引用。脚本内容是:

Write-Host "Running OverrideTestMethodAttribute.ps1" $mask = "$PSScriptRoot\Features\*.feature.cs" $codeBehindFiles = Get-ChildItem $mask Write-Host "Found $($codeBehindFiles.Count) feature code-behind files in $mask" foreach ($file in $codeBehindFiles) { Write-Host "Working on feature code-behind file: $($file.PSPath)" $oldContent = Get-Content $file.PSPath $newContent = $oldContent.Replace(` '[Microsoft.VisualStudio.TestTools.UnitTesting.TestMethodAttribute()]', ` '[MyCompany.MyProduct.IntegrationTestMethodAttribute()]') Set-Content -Path $file.PSPath -Value $newContent }

以及执行实际重试的 IntegrationTestMethodAttribute 类:

using System; using System.Collections.Generic; using System.Linq; using System.Text; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace MyCompany.MyProduct { public class IntegrationTestMethodAttribute : TestMethodAttribute { public override TestResult[] Execute(ITestMethod testMethod) { TestResult[] testResults = null; var failedAttempts = new List<TestResult>(); int maxAttempts = 5; for (int i = 0; i < maxAttempts; i++) { testResults = base.Execute(testMethod); Exception ex = testResults[0].TestFailureException; if (ex == null) { break; } failedAttempts.AddRange(testResults); } if (failedAttempts.Any() && failedAttempts.Count != maxAttempts) { TestResult testResult = testResults[0]; var messages = new StringBuilder(); for (var i = 0; i < failedAttempts.Count; i++) { var result = failedAttempts[i]; messages.AppendLine(""); messages.AppendLine(""); messages.AppendLine(""); messages.AppendLine($"Failure #{i + 1}:"); messages.AppendLine(result.TestFailureException.ToString()); messages.AppendLine(""); messages.AppendLine(result.TestContextMessages); } testResult.Outcome = UnitTestOutcome.Error; testResult.TestFailureException = new Exception($"Test failed {failedAttempts.Count} time(s), then succeeded"); testResult.TestContextMessages = messages.ToString(); testResult.LogError = ""; testResult.DebugTrace = ""; testResult.LogOutput = ""; } return testResults; } } }
    

-3
投票
Specflow 场景的目的是断言系统的行为符合预期。

如果某些临时问题导致测试失败,那么重新运行测试并“希望最好的结果”并不能解决问题!测试偶尔失败不应该是预期的行为。测试每次执行时都应该给出一致的结果。

可以在

here找到一篇关于如何进行良好测试的精彩帖子,该答案还指出测试应该是:

可重复:测试每次都应该产生相同的结果.. 时间。测试不应依赖于不可控的参数。

在这种情况下,测试失败是完全正确的。您现在应该调查为什么测试偶尔会失败。

大多数情况下测试会由于时间问题而失败,例如页面加载期间不存在的元素。在这种情况下,如果给定一致的测试环境(即相同的测试数据库、相同的测试浏览器、相同的网络设置),那么您将能够再次编写可重复的测试。查看

this 答案,了解如何使用 WebDriverWait 等待预定的时间来测试预期 DOM 元素是否存在。

© www.soinside.com 2019 - 2024. All rights reserved.