How to make step argument dependent on configuration?

1.1k views Asked by At

Problem

I am using SpecFlow to create an integration test suite for a REST service.

I am running the suite in multiple different configurations. (I have multiple Build configurations, each with its own set of app.config transformations.)
In the C# code, it's very simple to check the configuration and execute different code based on it. I can simply do something like this.

[Given(@"I set the test parameter to ""(.*)""")]
public void GivenISetTheTestParameter(string parameter)
{
    if(CurrentConfiguration == Config.Test)
        this.testParameter = parameter + "Test";
    else if(CurrentConfiguration == Config.Prod)
        this.testParameter = parameter + "Prod";
}

The problem with this approach is that it works the same way for every execution of this step, but I wan't to parameterize the configuration-dependent part of the step differently in every scenario.
Is there any way to do this in the feature file? I would like to do something like this (pseudo-code, this is not working of course):

If (CurrentConfiguration == Config.Test)
Given I set the test parameter to "ParameterTest"
Else If (CurrentConfiguration == Config.Prod)
Given I set the test parameter to "ParameterProd"

Then I can use this parameterization in a different way in every scenario:

Scenario: Test 1
    If (CurrentConfiguration == Config.Test)
    Given I set the test parameter to "ParameterTest1"
    Else If (CurrentConfiguration == Config.Prod)
    Given I set the test parameter to "ParameterProd1"
    ...

Scenario: Test 2
    If (CurrentConfiguration == Config.Test)
    Given I set the test parameter to "ParameterTest2"
    Else If (CurrentConfiguration == Config.Prod)
    Given I set the test parameter to "ParameterProd2"
    ...

If the condition was implemented in the C# code for the step, this wouldn't be possible.

Real world example

I would like to use this for integration testing a REST service. Let's say I use basic authentication, for which I need to set a header on my RestClient object.
I have a helper step for setting the auth header to a specific user name and password.

The tricky part is that I have multiple build configurations (let's say Staging and Prod), for which I need different test credentials. Also, I'm calling different APIs in the different scenarios of my feature, which also need different credentials.

So with the above introduced pseudo-syntax, this is what I'd like to do:

Scenario: Test LoggingService
    If (CurrentConfiguration == Config.Test)
        Given I set the auth header for the user "logging_test_user" and password "p4ssword"
    Else If (CurrentConfiguration == Config.Prod)
        Given I set the auth header for the user "logging_prod_user" and password "p4ssword"
    ...
    When I call the LoggingService
    ...

Scenario: Test PaymentService
    If (CurrentConfiguration == Config.Test)
        Given I set the auth header for the user "payment_test_user" and password "p4ssword"
    Else If (CurrentConfiguration == Config.Prod)
        Given I set the auth header for the user "payment_prod_user" and password "p4ssword"
    ...
    When I call the PaymentService
    ...

If I can only put the condition into the C# implementation of the "Given I set the auth header..." step, then I wouldn't be able to specify different user names for the different scenarios.

5

There are 5 answers

0
Greg Burghardt On BEST ANSWER

You don't want the configurable data in your feature files at all. Instead, create a generic step whose definition reads the config file:

Scenario: Test LoggingService
    Given I set the auth header

And in C#:

[Given(@"I set the auth header")]
public void GivenISetTheAuthHeader()
{
    string username = System.Configuration.ConfigurationManager.AppSettings["RestServiceUserName"];
    string password = System.Configuration.ConfigurationManager.AppSettings["RestServicePassword"];
}

And in App.config:

<appSettings>
  <add key="RestServiceUserName" value="..."/>
  <add key="RestServicePassword" value="..."/>

If different usernames have different permissions in the system, then consider using a Scenario Outline instead:

Scenario Outline: Testing the LoggingService
    Given I set the auth header for user "<Username>" and password "<Password>"

Examples:
    | Username | Password |
    | user1    | pass1    |
    | user2    | pass2    |

And they become normal parameters to your step definition:

[Given("I set the auth header for user """(.*)""" and password """(.*)"""")]
public void GivenISetTheAuthHeaderForUserAndPassword(string username, string password)
{
    // set the user and password on the auth header
}
0
Sam Holder On

We do something similar for different environments, but we have a app.config for the tests which has several 'alternate' configurations for dev, qa and uat and we read the value of a named parameter from one of these sections.

We have something like this

<testingEnvironments>
  <testingEnvironment name="Dev" messageUrl="https://somedevurl/" isCurrent="true">
     <ConfigParam1>SomeValue</ConfigParam1>
  </testingEnvironment>
  <testingEnvironment name="QA" messageUrl="https://somedqaurl/" isCurrent="false">
     <ConfigParam1>SomeValueInQA</ConfigParam1>
  </testingEnvironment>
  <testingEnvironment name="UAT" messageUrl="https://someuaturl/" isCurrent="false">
     <ConfigParam1>SomeValueUAT</ConfigParam1>
  </testingEnvironment>
</testingEnvironments>

we select the config based on the value of the isCurrent attribute, but you could just select it on the name based on an environment variable.

Then your tests are ignorant of the exact values used then just refer to ConfigParam1

based on your real world example I don't like the implementation detail in the tests (what if you use some other authentication mechanism) and would restructure my specifications like this:

Scenario: Test LoggingService
    Given I am the logging service default user for the current environment     
    When I call the LoggingService
    ...

Scenario: Test payment Service
    Given I am the payment service default user for the current environment     
    When I call the PaymentService
    ...

and would add config something like this:

  <testingEnvironment name="DEV" messageUrl="https://somedevurl/" isCurrent="false">
     <userCredentials>
          <LoggingService>
             <defaultUser name="logging_test_user" password="p4ssword" />                 
          </LoggingService>
          <PaymentService>
             <defaultUser name="payment_test_user" password="p4ssword" />                 
          </PaymentService>
     </userCredentials>         
  </testingEnvironment>
  <testingEnvironment name="UAT" messageUrl="https://someuaturl/" isCurrent="false">
     <userCredentials>
          <LoggingService>
             <defaultUser name="logging_prod_user" password="p4ssword" />                 
          </LoggingService>
          <PaymentService>
             <defaultUser name="payment_prod_user" password="p4ssword" />                 
          </PaymentService>
     </userCredentials>         
  </testingEnvironment>

your individual steps could then call a common step to set the actual header values

0
Daniel Mann On

Your tests should always be the same -- a test with an "if" in it is at least two tests. The proper way to tackle this is to isolate the system under test such that it takes a parameter (or is otherwise provided with a value) that represents the configuration value, then write tests for all of the applicable scenarios.

2
Filipe Borges On

I would write the feature:

Scenario: Test LoggingService
    Given I set the auth header with valid user and password
    When I call the LoggingService
    # ...

Set the App.config file:

<appSettings>
    <add key="EnvironmentUserName" value="..."/>
    <add key="EnvironmentPassword" value="..."/>
    <!-- ... -->
</appSettings>

and implement the step as:

public void GivenISetTheAuthHeader()
{
    string username = System.Configuration.ConfigurationManager.AppSettings["EnvironmentUserName"];
    string password = System.Configuration.ConfigurationManager.AppSettings["EnvironmentPassword"];
    // ...
 }
0
Sam Holder On

you could achieve what you want with a scenario outline and tagged examples, but then you would have to run only some tests in some environments:

Scenario Outline: Testing the LoggingService
    Given I set the auth header for user "<Username>" and password "<Password>"

@Production
Examples:
    | Username          | Password |
    | logging_prod_user | p4ssword |
@Test
Examples:
    | Username          | Password  |
    | logging_test_user | p4assword |

then configure your test runner to run only tests in certain categories (either Test or Production)

if you are using nunit (or XUnit or any other test runner that defaults to using row tests for running scenario outlines) as your test runner, be aware of this issue