轻量级 Wait.Until() 机制






4.82/5 (8投票s)
本文介绍了一种可能在各种场景下都很有用的 Wait.Until() 机制。
引言
在我之前基于 Selenium WebDriver 封装的自动化测试项目中,在浏览器尚未完成渲染时,某些操作未能获得预期结果甚至抛出异常的情况并不少见。
尽管有一些措施可以确保在浏览器就绪之前让进程等待足够长的时间,但涉及的代码过于复杂难以理解,并且会反复抛出相同的异常。因此,我编写了一些简单的函数来实现 Wait.Until() 机制,该机制在一定程度上仍然是线程阻塞的,但仍然适用于自动化测试场景或作为看门狗。
现在,当我将我的框架转换为直接基于 WebDriver 时,我相信同样的机制可以用来替换由 WebDriver.Support.UI 提供的官方 WebDriverWait 类。由于这是一个非常简单的机制,通用版本可能在其他场景下也很有用。
背景
关于等待的一个很好的介绍可以在 这个示例 中得到解释。
IWebDriver driver = new FirefoxDriver(); driver.Url = "http://somedomain/url_that_delays_loading"; WebDriverWait wait = new WebDriverWait(driver, TimeSpan.FromSeconds(10)); IWebElement myDynamicElement = wait.Until<IWebElement>((d) => { return d.FindElement(By.Id("someDynamicElement")); });
在这种情况下,在理想情况下我们真正想做的是:
IWebElement myDynamicElement = d.FindElement(By.Id("someDynamicElement"));
然而,浏览器在没有延迟加载网页的情况下无法理想地工作,并且在执行上述代码之前总是会显示它们。这就是为什么使用通用的 wait.Until 来封装代码作为 Lambda 表达式,并使其一直运行直到超时或我们获得预期结果。同样的思想被强制执行,但我打算通过 LINQ/Lambda 和函数委托使其更具扩展性。
基础 Wait.Until()
基础代码如下:
public static class Wait
{
//Mininum time in mSec waited before execution
public const int MinIntervalMills = 1;
//Maximum time in mSec waited before next execution
public const int MaxIntervalMills = 100;
//Maxium time to be waited before timeout
public const int TimeoutInMills = 10 * 1000;
public static TimeSpan Timeout = TimeSpan.FromMilliseconds(TimeoutInMills);
/// <summary>
/// This method execute any commands wrapped as a Predicate periodically until it is timeout.
/// If the command execution is success (when predicate returns true), then it would return immediately.
/// Otherwise, all exepections except the last one due to the command execution would be discarded considering
/// that they are identical; and the last exception would be throw when command execution is not success when
/// timeout happens.
/// </summary>
/// <param name="predicate">Wrapper of the execution codes that might throw exceptions.</param>
/// <param name="timeoutMills">Time waited before draw conclusion that the command cannnot succeed.</param>
/// <returns>'true' if returns before timeout, 'false' when timeout happened.</returns>
public static bool Until(Func<bool> predicate, int timeoutMills = TimeoutInMills)
{
if (timeoutMills <= 0)
throw new System.ComponentModel.InvalidEnumArgumentException("The timeout must be a positive value");
//Get the moment when the execution is considered to be timeout
DateTime timeoutMoment = DateTime.Now + TimeSpan.FromMilliseconds(timeoutMills);
int interval = MinIntervalMills;
Exception lastException = null;
do
{
try
{
//If something happen as expected, return immediately and ignore the previous exception
if (predicate())
return true;
}
catch (Exception ex)
{
// Intentionally record only the last Exception due to the fact that usually it is due to same reason
lastException = ex;
}
//Waiting for a period before execution codes within predicate()
System.Threading.Thread.Sleep(interval);
//The waiting time is extended, but no more than that defined by MaxIntervalMills
interval = Math.Min(interval * 2, MaxIntervalMills);
} while (DateTime.Now < timeoutMoment);
//Exected only when timeout before expected event happen
//If there is some exception during the past executions, throw it for debugging purposes
if (lastException != null)
throw lastException;
else
//throw new TimeoutException();
return false;
}
对于 Wait.Until(),实际上只需要一个签名是 Func<bool> 的谓词来判断操作是否成功。原则上,封装在谓词中的代码将以以下间隔执行:
After 0ms: if get expected result, return immdiately. After another 1ms: if get expected result, return immdiately. After another 2ms: if get expected result, return immdiately. ... After another 64ms: if get expected result, return immdiately. After another 100ms: if get expected result, return immdiately. After another 100ms: if get expected result, return immdiately. ... Timeout: if there is any Exception caught during the above execution, throw it.
执行间隔的变化并不重要,关键在于谓词中涉及的代码被执行了,但对于 Wait.Until() 来说,只有当它返回 true 时才有区别。这可以通过以下测试代码来验证,其中一个运行 changeNumberPeriodically() 的线程会随机更新“number”。
private const int intervalInMills = 20;
static string[] numbers = {"One", "Two", "Three", "Four", "Five"};
public static int number = 0;
static void Main()
{
var t = new Thread(changeNumberPeriodically);
t.Start();
//Testing of Wait.Until()
Func<bool> predicate1 = () =>
{
logTickAndNumber();
return number >= 4;
};
startTick = Environment.TickCount;
Wait.Until(predicate1);
Console.WriteLine("\r\nAfter Wait.Until(predicate1), number={0}, {1} larger than 4\r\n",
number, number >= 4 ? "" : "not");
//Testing of Wait.Until() when timeout happens
Func<bool> predicate2 = () =>
{
logTickAndNumber();
return number >= 10;
};
startTick = Environment.TickCount;
try
{
Wait.Until(predicate2, 5000);
}
catch (TimeoutException timeout)
{
Console.WriteLine("\r\nAfter Wait.Until(predicate2, 5000), number={0}, {1} larger than 10.\r\n"
, number, number >= 10 ? "" : "not");
}
//Testing of Wait.UntilString()
Func<string> getNumberString = () =>
{
string result = numbers[number - 1];
logTickAndNumber();
return result;
};
startTick = Environment.TickCount;
string fromUntilString = Wait.UntilString(getNumberString, "Five");
Console.WriteLine("\r\nAfter Wait.UntilString(getNumberString, \"Five\"), number={0}, numberString={1}.\r\n"
, number, fromUntilString);
//Testing of Wait.UntilContains()
number = 1;
startTick = Environment.TickCount;
fromUntilString = Wait.UntilContains(getNumberString, "F"); //"Four" or "Five"
Console.WriteLine("\r\nAfter Wait.UntilContains(getNumberString, \"F\"), number={0}, numberString={1}.\r\n"
, number, fromUntilString);
//Testing of GenericWait.Until()
Func<int> getNumber = () =>
{
logTickAndNumber();
return number;
};
number = 1;
startTick = Environment.TickCount;
GenericWait<int>.Until(getNumber, i => i >= 3);
Console.WriteLine("\r\nAfter GenericWait<int>.Until(getNumber, i => i >= 3), number={0}.\r\n"
, number);
//Testing of GenericWait.Until() when timeout is sure to happen
number = 1;
startTick = Environment.TickCount;
try
{
GenericWait<int>.Until(getNumber, i => i < 0);
}
catch (TimeoutException timeout)
{
Console.WriteLine("\r\nAfter GenericWait<int>.Until(getNumber, i => i < 0), number={0}.\r\n"
, number);
}
//Set done to quit the thread of t
done = true;
Console.ReadKey();
}
private static int startTick = Environment.TickCount;
static void logTickAndNumber()
{
Console.WriteLine("After {0}ms: number={1}", Environment.TickCount - startTick, number);
}
public static bool done = false;
static void changeNumberPeriodically()
{
Random rdm = new Random();
do
{
Thread.Sleep(intervalInMills);
number = rdm.Next(1, 6);
} while (!done);
}
第一个谓词在 number 等于或大于“4”时返回“true”,结果如下:
After 0ms: number=0 After 0ms: number=0 After 0ms: number=0 After 16ms: number=0 After 16ms: number=0 After 32ms: number=3 After 78ms: number=2 After 141ms: number=5 After Wait.Until(predicate1), number=5, larger than 4
第二个谓词应该始终返回“false”,因此 Wait.Until() 应该在 5 秒后超时。
After 0ms: number=5 After 0ms: number=5 After 0ms: number=5 After 0ms: number=4 After 16ms: number=4 After 31ms: number=2 After 62ms: number=3 After 125ms: number=3 After 234ms: number=4 After 328ms: number=4 ... After 4875ms: number=1 After 4984ms: number=2 After Wait.Until(predicate2, 5000), number=1, not larger than 10.
Wait.Until() 返回一个字符串
为了设计一个自动化测试用例,调用一个函数来获取字符串作为结果是很常见的,因此我将基础的 Wait.Until() 扩展如下:
/// <summary>
/// This method keep executing any function that a string as result by using the mechnism of Until()
/// until timeout or the result is exactly as "expectedString".
/// </summary>
/// <param name="getStringFunc">Any function returning string.
/// For functions with parameters, for example:
/// public string someFunc(int param),
/// This method can be called with assitance of LINQ as below:
/// UntilString(()=>someFunc(param), expectedString)
/// </param>
/// <param name="expectedString">string expected that cannot be null.</param>
/// <param name="timeoutMills">Time waited before draw conclusion that the command cannnot succeed.</param>
/// <returns>The final result of calling getStringFunc().</returns>
public static string UntilString(Func<string> getStringFunc, string expectedString, int timeoutMills = TimeoutInMills)
{
if (expectedString == null)
throw new ArgumentNullException();
string result = null;
Func<bool> predicate = () =>
{
result = getStringFunc();
return result == expectedString;
};
Until(predicate, timeoutMills);
return result;
}
/// <summary>
/// This method keep executing any function that a string as result by using the mechnism of Until()
/// until timeout or the result contains the "expectedString".
/// </summary>
/// <param name="getStringFunc">Any function returning string.
/// For functions with parameters, for example:
/// public string someFunc(int param),
/// This method can be called with assitance of LINQ as below:
/// UntilString(()=>someFunc(param), expectedString)
/// </param>
/// <param name="expectedString">string expected to be contained by calling getStringFunc().</param>
/// <param name="timeoutMills">Time waited before draw conclusion that the command cannnot succeed.</param>
/// <returns>The final result of calling getStringFunc().</returns>
public static string UntilContains(Func<string> getStringFunc, string expectedString, int timeoutMills = TimeoutInMills)
{
if (expectedString == null)
throw new ArgumentNullException();
string result = null;
Func<bool> predicate = () =>
{
result = getStringFunc();
return result.Contains(expectedString);
};
Until(predicate, timeoutMills);
return result;
}
函数 UntilString(Func<string>, string, int) 期望一个不带参数的函数,并将其与预期的字符串合并,以构成 Wait.Until(Func<bool> predicate, int timeoutMills) 的 Func<bool> predicate 参数。
然而,在大多数情况下,我们可能需要执行签名类似于 string functionWithParameters(int, string, ...) 的方法。这时,需要进行一些巧妙的处理,如本示例所示:
var wrapper = () => functionWithParameters(intParam, stringParam, otherParam);
string result = Wait.UntilString(wrapper, expectedString);
.NET 的 LINQ 真是太棒了,不是吗?
其功能由以下测试验证:
static void Main()
{
...
//Testing of Wait.UntilString()
Func<string> getNumberString = () =>
{
string result = numbers[number - 1];
logTickAndNumber();
return result;
};
startTick = Environment.TickCount;
string fromUntilString = Wait.UntilString(getNumberString, "Five");
Console.WriteLine("\r\nAfter Wait.UntilString(getNumberString, \"Five\"), number={0}, numberString={1}.\r\n"
, number, fromUntilString);
//Testing of Wait.UntilContains()
number = 1;
startTick = Environment.TickCount;
fromUntilString = Wait.UntilContains(getNumberString, "F"); //"Four" or "Five"
Console.WriteLine("\r\nAfter Wait.UntilContains(getNumberString, \"F\"), number={0}, numberString={1}.\r\n"
, number, fromUntilString);
...
}
getNumberString 展示了如何使用 LINQ 组合一个函数来执行任何所需的运算,结果正如预期。
After 0ms: number=1
After 0ms: number=1
After 0ms: number=1
After 15ms: number=1
After 15ms: number=1
After 31ms: number=2
After 62ms: number=5
After Wait.UntilString(getNumberString, "Five"), number=5, numberString=Five.
After 0ms: number=1
After 16ms: number=5
After Wait.UntilContains(getNumberString, "F"), number=5, numberString=Five.
通用 Wait.Until()
在实际情况中,我们更可能需要调用一个可能返回任何类型结果的函数。因此,定义了一个通用版本如下:
public static class GenericWait<T>
{
/// <summary>
/// This method execute func() continuously by calling Wait.Until() until timeout or expected condition is met.
/// </summary>
/// <param name="func">
/// Any function returning T as result.
/// For functions whose signature has one or more parameters, for example:
/// public T someFunc(int param),
/// This method can be called with assitance of LINQ as below:
/// Until(()=>someFunc(param), isExpected)
/// </param>
/// <param name="isExpected">Predicate to judge if the result returned by func() is expected</param>
/// <param name="timeoutMills">Time waited before draw conclusion that the command cannnot succeed.</param>
/// <returns>The last result returned by func().</returns>
public static T Until(Func<T> func, Func<T, bool> isExpected, int timeoutMills=Wait.TimeoutInMills)
{
if (func == null || isExpected == null)
throw new ArgumentNullException();
T result = default(T);
Func<bool> predicate = () =>
{
result = func();
return isExpected(result);
};
Wait.Until(predicate, timeoutMills);
return result;
}
}
上一节讨论的返回字符串的函数实际上可以用这种方式替换:
Func<string> someFuncReturnsString = ()=>getString();
Func<string, bool> someDelegateOfString = s => s.Contains("abc");
string finalStringResult = GenericWait<string>.Until(someFuncReturnsString, someDelegateOfString);
GenericWait<T>.Until() 的功能如下验证:
static void Main()
{
number = 1;
startTick = Environment.TickCount;
GenericWait<int>.Until(getNumber, i => i >= 3);
Console.WriteLine("\r\nAfter GenericWait<int>.Until(getNumber, i => i >= 3), number={0}.\r\n"
, number);
//Testing of GenericWait.Until() when timeout is sure to happen
number = 1;
startTick = Environment.TickCount;
GenericWait<int>.Until(getNumber, i => i < 0);
Console.WriteLine("\r\nAfter GenericWait<int>.Until(getNumber, i => i < 0), number={0}.\r\n"
, number);
//Set done to quit the thread of t
done = true;
Console.ReadKey();
}
结果是:
After 16ms: number=1
After 16ms: number=1
After 16ms: number=1
After 16ms: number=3
After GenericWait<int>.Until(getNumber, i => i >= 3), number=3.
After 0ms: number=1
After 0ms: number=1
After 0ms: number=1
After 15ms: number=1
After 15ms: number=1
...
After 9843ms: number=2
After 9937ms: number=1
After GenericWait<int>.Until(getNumber, i => i < 0), number=5.
请注意,第二个在 10 秒后退出,这是 Wait 类中定义的默认 TimeSpan Timeout。
摘要
本文介绍了一种轻量级的 Wait.Until() 机制。由于其简单性以及与 .NET 的 LINQ 和函数委托结合使用的便利性,它可以普遍用于消除大量冗余代码。
历史
2014 年 5 月 19 日:初版。
2014 年 7 月 3 日:Wait.Until() 超时时抛出 TimeoutException。
2015 年 3 月 31 日:更改 Wait.Until() 的签名,使其返回一个布尔值,以指示在超时之前是否满足了谓词指定的条件。