Domanda

I wish to be able to write tests like this:

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

How do I do the specflow side of the tests?

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

È stato utile?

Soluzione

I think you may be looking for StepArgumentTransformation.

To cope with 'in 31 days', the docs have it for you:

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

And for 'in 2 years', you can see the pattern...

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

Altri suggerimenti

Building this .feature file will create a code behind for the tests. You then need to wire up each step to a method. The easiest way to do this is,

1: debug the tests, the test will fail as inconclusive. Looking at the Test run results specflow helps you by adding a template for this test. the error message will look something like this

Assert.Inconclusive failed. No matching step definition found for one or more steps.

    [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: Copy this into a new specflow step definition file, which is basically just unit test class populated with specflow attributes. Now there are some tricks you can do to help you out. in the GivenAUserSignsUpForA30DayAccount method i would create a user that will be used in the test that has a 30 day trial account. A private member would work fine here so you can access them between methods, but this only works if all the methods are in the same class. if you try to reuse methods between multiple features/classes you will need to look into saving your object into the ScenarioContext

3: When the specflow test runs it looks for a method that has an matching attribute with the same string. A trick here is that you can u pass parameters to the method using wild cards in the Method Attribute. There are 2 different file cards

(.*) means you are passing a string to that method (\d+) means you are passing an int to that method.

Because your When method is common you could reuse it with parameters like this.

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

4: finally add your Asserts into the Then method so the end result looks something like this. (note that personally i would restructure the wording of the feature a bit and pass it expected results that way the test logic isn't as nasty as my example, look at scenario outlines for data driven tests)

    [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);

    }
}

I faced a similar issue with how to cope with relative dates and times in SpecFlow, and approached it by supporting fuzzy dates within specifications. I used the code from this answer: Fuzzy Date Time Picker Control in C# .NET?, which would let you express what you want as follows:

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

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

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

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

With a step definition such as:

[When(@"they login in the (.*)")]
public void WhenTheyLoginIn(string loginDateTimeString)
{
    DateTime loginDateTime = FuzzyDateTime.Parse(loginDateTimeString);

    // TODO: Use loginDateTime
}

If you don't like the syntax for the fuzzy dates, you can modify the regular expressions in the FuzzyDateTime code to suit.

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

If your rules need no special handling for workingday, xmas, weekend, ... you can modify @Nitro52-s answer to:

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

Maybe you can also think about reformulating the scenarios like this

Scenario: access before expiry
  When they login on '2010-01-01'
    And TodayIs '2010-01-29'
  Then they will be let in

I know I struggled with this for a long time. It took a while to change my existing code, but killing all references to DateTime.Now and replacing them with an interface that I could Mock has made everything a million times easier to test (and change later). I have created an IDateTimeService that has one method "GetCurrent()". Now all my Given steps can say:

Given the current date is '2/4/12'
And the user's account was created on '1/24/12'

Then I can do range checking a lot easier.

The step for the current date looks like:

[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);
}

Try using Moles and stub the DateTime.Now to return the same date everytime. One of the best features of Moles is the ability to turn dang near anything into a runtime delegate that you can isolate. The only drawback is that it may run slower depending on the implementation you pick (stubbed vs moled). I'm just beginning to dive into it so take my suggestion with a grain of salt.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top