【问题标题】:SpecFlow - Retry failed testsSpecFlow - 重试失败的测试
【发布时间】:2014-01-22 09:00:26
【问题描述】:

有没有办法实现AfterScenario 挂钩以在失败的情况下重新运行当前测试?

类似这样的:

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

注意:我项目中的测试组合成 Ordered 测试,并通过 MsTest 执行。

【问题讨论】:

  • 第二次运行的条件是什么?
  • 好问题@rene!我想我的整个想法都是胎死腹中。

标签: testing hook specflow scenarios


【解决方案1】:

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

它将允许您这样做:

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

【讨论】:

    【解决方案2】:

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

    我正在运行 UI 测试(针对 Angular 应用程序使用 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>
    

    现在我们可以使用 testrunner 从错误中恢复:

    [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 步骤中使用它,您可以向 testrunner 添加一个 RetryScenario() 方法并调用它。

    最后说明:当您无能为力时,将此作为最后的手段。运行不稳定的测试总比不运行测试好。

    【讨论】:

    • "运行不稳定的测试总比不运行测试好。" - 你确定? ;)
    【解决方案3】:

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

    我设法使用 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;
            }
        }
    }
    

    【讨论】:

      【解决方案4】:

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

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

      可以在here 找到一篇关于什么是好的测试的好帖子,该答案还指出测试应该是:

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

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

      大多数情况下,测试因时间问题而失败,例如页面加载期间不存在的元素。在这种情况下,给定一个一致的测试环境(即相同的测试数据库、相同的测试浏览器、相同的网络设置),那么您将再次能够编写可重复的测试。查看this 关于使用 WebDriverWait 等待预定时间来测试预期 DOM 元素是否存在的答案。

      【讨论】:

      • 对谁投反对票,请解释为什么你认为测试不应该保证可重复的行为。
      • @BenSmith,在我的案例中,测试因 Selenium 而失败。它有时只是无法在页面上找到元素,尽管它的存在和文档已经准备好。当我多次运行相同的测试时(就像我只是复制粘贴场景)它可以是绿色的也可以是红色的。 Selenium 或第三方服务的不稳定是其失败时尝试运行场景的主要原因。
      • @VladislavQulin 在这种情况下,您的测试编写正确。找出测试中的错误,并重写它以使 Selinium 行为一致。我过去也遇到过类似的问题,你几乎可以保证这不是工具的问题,而是你使用它的方式。
      • @BenSmith,投反对票。你给出了一个强有力的建议,但不是一个实用的建议。 Selenium 存在环境问题,其中一个例子是 Firefox 加载时间过长(我们是否甚至想调查为什么有时它加载速度很快,有时却没有?),突然 ajax 稍微花费更长的时间,等等......所以,我会不要使用如此大胆的理论陈述。
      • 我打赌@Adam 同意校长/指导方针,我也是。作为一个必须平衡我的校长/指导方针与彼此和现实的人,我对这个问题的答案很感兴趣。 LaTisha 可能提供了一个;我会检查一下。如果 BenSmith 和其他人可以依赖 100% 一致的浏览器/服务器行为,那太棒了,对你有好处。有时这就是我的情况,有时不是。这些 cmets 作为对该问题的评论会更好,因为它们是相关的并且可能有用的 cmets,但它们不是答案。 (对不起,我的小毛病。)
      猜你喜欢
      • 2021-10-11
      • 1970-01-01
      • 2021-08-08
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多