如何使SpecFlow能够很好地处理日期/时间?

6

我希望能够编写这样的测试:

Background:
  Given a user signs up for a 30 day account

Scenario: access before expiry
  When they login in 29 days
  Then they will be let in

Scenario: access after expiry
  When they login in 31 days
  Then they will be asked to renew

Scenario: access after acounnt deleted
  When they login in 2 years time
  Then they will be asked to register for a new account

我该如何处理测试中的specflow部分?

编辑:同一个步骤定义如何处理“31天”和“2年时间”?

6个回答

3
构建这个.feature文件将为测试创建一个代码后端。然后,您需要将每个步骤连接到一个方法。最简单的方法是:

1:调试测试,测试将失败并显示未决。查看测试运行结果specflow会通过为该测试添加一个模板来帮助您。错误消息将类似于:

Assert.Inconclusive失败。找不到一个或多个步骤的匹配步骤定义。

    [Binding]
public class StepDefinition1
{
    [Given(@"a user signs up for a 30 day account")]
    public void GivenAUserSignsUpForA30DayAccount()
    {
    }

    [When(@"they login in 29 days")]
    public void WhenTheyLoginIn29Days()
    {
        ScenarioContext.Current.Pending();
    }

    [Then(@"they will be let in")]
    public void ThenTheyWillBeLetIn()
    {
        ScenarioContext.Current.Pending();
    }
}

2:将此内容复制到新的specflow步骤定义文件中,该文件基本上只是填充了specflow属性的单元测试类。现在有一些技巧可以帮助您。在GivenAUserSignsUpForA30DayAccount方法中,我将创建一个用户,该用户将在测试中使用具有30天试用帐户。私有成员在这里可以很好地工作,以便您可以在方法之间访问它们,但是如果所有方法都在同一个类中,则仅适用于此情况。如果您尝试在多个功能/类之间重用方法,则需要查看将对象保存到ScenarioContext中

3:当specflow测试运行时,它会查找具有相同字符串的匹配属性的方法。这里的诀窍是,您可以使用方法属性中的通配符向方法传递参数。有两个不同的通配符:

(.*)表示您正在将一个字符串传递给该方法 (\d+)表示您正在将一个整数传递给该方法。

由于您的When方法是常见的,因此您可以像这样重用它的参数。

    [When(@"they login in (\d+) days")]
    public void WhenTheyLoginInDays(int daysRemaining)
    {
        Account.DaysRemaining = daysRemaining;
    }

4: 最后将你的断言添加到Then方法中,使最终结果看起来像这样。(请注意,个人建议重新构造feature的措辞,并以期望结果的方式传递它,这样测试逻辑就不会像我的示例那样混乱。可以查看数据驱动测试的场景概述)

    [Binding]
public class StepDefinition1
{
    UserAccount user;

    [Given(@"a user signs up for a 30 day account")]
    public void GivenAUserSignsUpForA30DayAccount()
    {
        user = AccountController.CreateNewUser("bob", "password", AccountType.Trial);
    }

    [When(@"they login in (\d+) days")]
    public void WhenTheyLoginInDays(int daysRemaining)
    {
        Account.DaysRemaining = daysRemaining;
    }

    [Then(@"they will (.*)")]
    public void ThenTheyWillBeLetIn(string expected)
    {
        //check to see which test we are doing and then assert to see the expected result.
        if(string.Compare(expected, "be let in", true)
            Assert.AreEqual(LoginResult.Passed, LoginService.Login);
        if(string.Compare(expected, "be asked to renew", true)
            Assert.AreEqual(LoginResult.Passed, LoginService.Login);

    }
}

非常感谢您的出色回答,但是我错过了我的问题的一部分,现在我已经纠正了 - 对于移动帖子感到抱歉!例如,相同的步骤定义如何处理“31天”和“2年时间”? - Ian Ringrose

3
我在SpecFlow中遇到了与如何处理相对日期和时间的类似问题,并通过在规范中支持模糊日期来解决它。我使用了来自这个答案的代码:C# .NET中的模糊日期时间选择器控件?,它允许您按以下方式表达您想要的内容:
背景:
    假设用户注册了一个为期30天的帐户
场景:在到期前访问 当他们在接下来的29天内登录时 那么他们将被放行
场景:在到期后访问 当他们在接下来的31天内登录时 那么他们将被要求续订
场景:在删除帐户后访问 当他们在接下来的2年内登录时 那么他们将被要求注册新帐户
有一个步骤定义如下:
[When(@"they login in the (.*)")]
public void WhenTheyLoginIn(string loginDateTimeString)
{
    DateTime loginDateTime = FuzzyDateTime.Parse(loginDateTimeString);
// TODO: Use loginDateTime }
如果您不喜欢模糊日期的语法,您可以修改FuzzyDateTime代码中的正则表达式以适应您的需求。

2

我认为你可能在寻找StepArgumentTransformation

为了处理“31天内”,文档已经为您准备好:

[Binding]
public class Transforms
{
    [StepArgumentTransformation(@"in (\d+) days?")]
    public DateTime InXDaysTransform(int days)
   {
      return DateTime.Today.AddDays(days);
   }
}

对于“两年内”,您可以看到以下模式...

    [StepArgumentTransformation(@"in (\d+) years?")]
    public DateTime InXYearsTransform(int years)
   {
      return DateTime.Today.AddYears(years);
   }

我认为在我提出这个问题后不久,StepArgumentTransformation被添加到了Specflow中... - Ian Ringrose
啊,没错。虽然这与我的搜索问题相关(https://dev59.com/e5zha4cB1Zd3GeqPE2tZ),但为了其他人的利益... - Subjective Reality

1
> how can the same step definitions cope with both "31 days" and "2 years time"

如果您的规则在工作日、圣诞节、周末等方面不需要特殊处理,您可以修改@Nitro52的答案如下:
[When(@"they login in (\d+) days")]
public void WhenTheyLoginInDays(int daysRemaining)
{
    Account.RegristrationDate = DateTime.ToDay().SubtractDays(daysRemaining);
    Account.VerificationDate = DateTime.ToDay();    
}

也许您也可以考虑像这样重新构思场景。
Scenario: access before expiry
  When they login on '2010-01-01'
    And TodayIs '2010-01-29'
  Then they will be let in

1
我知道我曾经在这个问题上挣扎了很久。改变现有代码需要一些时间,但是通过杀掉所有对DateTime.Now的引用并替换为我可以模拟的接口,我使得测试变得更加容易(而且后续修改也更加方便)。我创建了一个IDateTimeService接口,其中只有一个方法“GetCurrent()”。现在,我的所有Given步骤都可以这么说:
Given the current date is '2/4/12'
And the user's account was created on '1/24/12'

那么我可以更轻松地进行范围检查。

当前日期的步骤如下:

[Given(@"Given the current date is '(.*)'")]
public void GivenTheCurrentDateIs(string date)
{
     var dateServiceMock = new Mock<IDateTimeService>();
     dateServiceMock.Setup(ds => ds.GetCurrent()).Returns(DateTime.Parse(date));
     ScenarioContext.Current.Add("dateService", dateServiceMock);
}

-2

尝试使用Moles并将DateTime.Now存根以每次返回相同的日期。 Moles最好的功能之一是能够将几乎任何东西转换为运行时委托,您可以隔离它。唯一的缺点是,根据您选择的实现(存根还是模拟),它可能运行得更慢。我刚开始深入研究它,所以请带着谨慎接受我的建议。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接