在失败的情况下,有没有办法实现AfterScenario
挂钩重新运行当前测试?
这样的事情:
[AfterScenario("retry")]
public void Retry()
{
if (ScenarioContext.Current.TestError != null)
{
// ?
}
}
注意:我的项目中的测试在Ordered测试中合并,并通过 MsTest 执行。
答案 0 :(得分: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
答案 1 :(得分:1)
Specflow场景的目的是断言系统的行为符合预期。
如果某些时间问题导致测试失败,那么让测试重新运行并“希望获得最佳”并不能解决问题!偶尔测试失败不应该是预期的行为。测试应该在每次执行时给出一致的结果。
可以找到一篇关于什么是一个好的测试的好帖子here,该答案还说明测试应该是:
可重复:每次测试都应该产生相同的结果 时间。测试不应该依赖于无法控制的参数。
在这种情况下,测试失败是完全正确的。您现在应该调查为什么测试偶尔会失败。
大多数情况下,测试因时序问题而失败,例如页面加载期间不存在的元素。在这种情况下,给定一致的测试环境(即相同的测试数据库,相同的测试浏览器,相同的网络设置),然后您将能够编写可重复的测试。查看使用WebDriverWait的this答案,等待预定的时间来测试是否存在预期的DOM元素。
答案 2 :(得分:1)
让我首先说我同意测试应该稳定并且不应该重试。但是,我们并不生活在理想的世界中,在某些非常特殊的情况下,重试测试可能是有效的用例。
我正在运行UI测试(对一个有角度的应用程序使用硒),有时chromedriver由于不清楚的原因而无法响应。这种行为完全是我无法控制的,不存在有效的解决方案。我无法在SpecFlow步骤中重试此步骤,因为我有“给定”步骤登录到应用程序。当它在“ When”步骤中失败时,我也需要重新运行“ Given”步骤。在这种情况下,我想关闭驱动程序,再次启动它,然后重新运行所有先前的步骤。作为最后的手段,我为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配置为使用我们自己的testrunner(也可以作为插件添加)。
/// <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步骤中使用此方法,可以将RetryScenario()方法添加到testrunner并调用它。
作为最后的提示:当您无能为力时,请使用此作为最后的手段。运行不稳定的测试比根本不进行测试要好。
答案 3 :(得分:0)
我希望能够重试失败的测试,但仍将其报告为测试结果失败。这样一来,我就可以轻松地确定代码可以工作的场景,但是由于网络延迟等原因,也容易出现零星的问题。由于代码更改,这些故障的优先级将不同于新的故障。
由于您可以创建一个继承自TestMethodAttribute的类,因此我设法使用MsTest做到了这一点。
首先,我将本节添加到csproj文件的底部,以在生成* .feature.cs文件之后但在实际构建之前调用自定义的powershell脚本:
<Target Name="OverrideTestMethodAttribute" BeforeTargets="PrepareForBuild">
<Message Text="Calling OverrideTestMethodAttribute.ps1" Importance="high" />
<Exec Command="powershell -Command "$(ProjectDir)OverrideTestMethodAttribute.ps1"" />
</Target>
然后,OverrideTestMethodAttribute.ps1 powershell脚本执行查找/替换,以将所有TestMethodAttribute引用更改为我的IntegrationTestMethodAttribute。脚本内容为:
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;
}
}
}