cucumber / godog

Cucumber for golang
MIT License
2.22k stars 250 forks source link

Re-using step definitions across features #193

Closed sasoiliev closed 4 years ago

sasoiliev commented 4 years ago

Hello,

We are currently using Godog for developing a system test suite for our company backend.

My question is - what is the proper way to share step definitions across features?

For example if we have two features:

manage-profile.feature:

Feature: Manage profile

  Scenario: Update profile
    Given a user is logged in
    When he updates his profile
    Then the updates are successfully persisted

contact-us.feature:

Feature: Contact us

  Scenario: Send a message through the contact form
    Given a user is logged in
    When the user sends a message through the contact form
    Then the message appears on our message board

With these two scenarios we have the a user is logged in step that is common and I can't figure out how to re-use the step implementation.

At first sight it seems that I need a single shared step definition (like func AUserIsLoggedIn() error). But then if we have two separate structures, say type ProfileState struct{} and type ContactUsState struct{} the shared step definition seems to have no way of knowing which of these two to use.

Even if we extract the logic in a normal function (i.e. a non-step definition one), the Gherkin steps for both scenarios would still point to the same step definition function (either the profile one or the contact one) if I'm not mistaken.

The only way out of this seems to be having different step definitions in each feature file (e.g. When a user is logged in (profile)/When a user is logged in (contact us) with the corresponding step definition functions), but that looks like a workaround to me rather than a proper solution.

In the Ruby Cucumber implementation this seems to be solved through the World concept - each scenario runs in the context of separate World instance, so they are already isolated and you can put state dynamically in the World instance throughout the scenario. In Java you could use a dependency injection framework to pass a world instance to each scenario. I'm really not sure how this can be achieved with Godog.

Disclaimer: Working with Gherkin is something relatively new to me, so I might very well be mi-using the system and my scenarios might not make sense at all. Please let me know if this is the case.

Thank you!

l3pp4rd commented 4 years ago

in World concept I think it is even more confusing, if you have same step definitions, doing different things. my personal best practice is to simplify the steps as much as possible and make then commonly used among the all the features if they are using the same backend application.

For example your step Given a user is logged in is not a good definition. because, we do not define what user and where it is available. It would be better to have Given there is an admin john.doe@org.com You could read the role admin as a variable and extract the email, which also could build the first and last name from it. Even the organization or group from org part. That way from this small step you can get much of the info about the user.

You can have some internal rules used in all the tests:

Then the step Given john.doe@org.com is logged in would simply log in that specific user, using the same password by default. the user would be in admin group since you created him in that way. So in that case you have all the context needed.

Or you can define your world based on the user group, such as org or admin depending on the use case.

l3pp4rd commented 4 years ago

you can also use Scenario or Feature tags, to switch the context. but I would prefer to have all the logic in steps.

sasoiliev commented 4 years ago

Hi l3pp4rd,

in World concept I think it is even more confusing, if you have same step definitions, doing different things.

They are not doing different things though, unless you perceive the same operations with different arguments different things.

my personal best practice is to simplify the steps as much as possible and make then commonly used among the all the features if they are using the same backend application.

The step from my example seems simple enough to me - I am logging in a user.

For example your step Given a user is logged in is not a good definition. because, we do not define what user and where it is available. It would be better to have Given there is an admin john.doe@org.com You could read the role admin as a variable and extract the email, which also could build the first and last name from it. Even the organization or group from org part. That way from this small step you can get much of the info about the user.

I don't see how parametrizing the step definition helps here. For a login step a common scenario is to receive some kind of token in the response that you then use to authenticate further requests. Don't you still need some place to store the token for the following steps to access?

You can have some internal rules used in all the tests:

* that `admin` has some specific permissions or group profile related differences.

* the `org` part could also define something similar as your World pattern

* all the users could have specific and same password by default

Then the step Given john.doe@org.com is logged in would simply log in that specific user, using the same password by default. the user would be in admin group since you created him in that way. So in that case you have all the context needed.

Or you can define your world based on the user group, such as org or admin depending on the use case.

We could do this, but this only solves the input part of the context. We would still need to store state that comes as a response from a backend (when we are testing a service at least) somewhere, right?

(Besides I don't really fancy the idea of applying implicit rules throughout the tests, but that's a separate topic.)

you can also use Scenario or Feature tags, to switch the context. but I would prefer to have all the logic in steps.

I thought tags are used for selection/filtering of tests subsets. Are the step definitions aware that they are running in a tagged feature/scenario? Can you elaborate how to use them for context management?

Thank you!

sasoiliev commented 4 years ago

@l3pp4rd, all, would you consider the following idea:

The step definitions would then be able to store state that needs to be used by further steps and they would be guaranteed that no state would leak between scenarios. This way you could share steps across features.

What do you think, does this sound reasonable?

(I actually have this implemented in my local Godog copy, but I want to know if you would accept the idea before I create a PR.)

Thanks!

l3pp4rd commented 4 years ago

Hi, no I would not accept it, because it adds additional cognitive load on any user. Godog does not handle state and it will not, this is on user who implements the steps. You can have the same ScenarioContext in every step. It can be updated on every BeforeScenario or BeforeFeature hook, which godog provides. Based on that context you can do whatever you want in your steps.

All of your step definitions can be functions of specific struct. In that struct you can have your context and refresh it on the hook. (you can use tags in order to know what context the feature or scenario should have).

sasoiliev commented 4 years ago

@l3pp4rd can you then please address the questions that I raised in https://github.com/DATA-DOG/godog/issues/193#issuecomment-531412859?

l3pp4rd commented 4 years ago

I explained it before already. you can have a struct, where you add steps as functions of this structure, so you can access and change the state. godog guarantees that only one scenario at the time will use that structure. though, you need to clear the state when the scenario, or feature finishes. if you cannot understand that, look at the godog tests and the context.

sasoiliev commented 4 years ago

Hi l3pp4rd,

Thank you for your response.

With all due respect (and I mean it, I really appreciate your work on Godog!) I don't think you have really addressed my questions.

In my initial request I was asking how to re-use a step definition across features. I still don't know how to do it, so let me try to explain the problem using a code example. Please bear with me.

I will go again with the two features from my initial post:

contact-us.feature

Feature: Contact us

  Scenario: Send a message through the contact form
    Given a user is logged in with email "user-1@example.com"
    When the user sends a message "message-1" through the contact form
    Then the message appears on our message board

manage-profile.feature

Feature: Manage profile

  Scenario: Update profile
    Given a user is logged in with email "user-2@example.com"
    When he updates his profile
    Then the updates are successfully persisted

Then let's say we have the following implementation of the user log in:

var (
    users = map[string]struct {
        token string
    }{
        "user-1@example.com": {token: "token-1"},
        "user-2@example.com": {token: "token-2"},
    }
)

func logIn(email string) (string, error) {
    user, found := users[email]
    if !found {
        return "", fmt.Errorf("unknown user: %s", email)
    }

    return user.token, nil
}

Then in my tests I am then creating two separate structs - one for each feature. I want to store the token returned by the log in function in each context so that my next steps can use it to authenticate their requests with the application:

type contactUsContext struct {
    token string
}

type manageProfileContext struct {
    token string
}

Then I implement my step definitions as methods on these structs:

func (c *contactUsContext) aUserIsLoggedInWithRoleAndEmail(email string) error {
    var err error
    c.token, err = aUserIsLoggedInWithRoleAndEmail(email)
    return err
}

func (c *manageProfileContext) aUserIsLoggedInWithRoleAndEmail(email string) error {
    var err error
    c.token, err = aUserIsLoggedInWithRoleAndEmail(email)
    return err
}

func (c *contactUsContext) theUserSendsAMessageThroughTheContactForm(msg string) error {
    fmt.Printf("Contact Us token:%s\n", c.token)
    return godog.ErrPending
}

func (c *contactUsContext) theMessageAppearsOnOurMessageBoard() error {
    return godog.ErrPending
}

func (c *manageProfileContext) heUpdatesHisProfile() error {
    fmt.Printf("Manage Profile token:%s\n", c.token)
    return godog.ErrPending
}

func (c *manageProfileContext) theUpdatesAreSuccessfullyPersisted() error {
    return godog.ErrPending
}

Note that each structure now has the aUserIsLoggedInWithRoleAndEmail function. Let's say that this is acceptable, although it is already not ideal, but we can work around this by extracting the logic for logging in into a third function that we can call from the two struct methods:

func aUserIsLoggedInWithRoleAndEmail(email string) (string, error) {
    token, err := logIn(email)
    if err != nil {
        return "", err
    }
    return token, nil
}

Now, how am I supposed to bind a function to the step regular expression in my suite context initializer so that each feature executes the correct step definition?

If I try to bind both of them then obviously only one will win (as it will match first):

func SuiteContext(s *godog.Suite) {
    contactUsCtx := contactUsContext{}
    manageProfileCtx := manageProfileContext{}

    // What to do here?
    s.Step(`^a user is logged in with email "([^"]*)"$`, contactUsCtx.aUserIsLoggedInWithRoleAndEmail)
    //s.Step(`^a user is logged in with email "([^"]*)"$`, manageProfileCtx.aUserIsLoggedInWithRoleAndEmail)
    s.Step(`^the user sends a message "([^"]*)" through the contact form$`, contactUsCtx.theUserSendsAMessageThroughTheContactForm)
    s.Step(`^the message appears on our message board$`, contactUsCtx.theMessageAppearsOnOurMessageBoard)
    s.Step(`^he updates his profile$`, manageProfileCtx.heUpdatesHisProfile)
    s.Step(`^the updates are successfully persisted$`, manageProfileCtx.theUpdatesAreSuccessfullyPersisted)
}

So to me it seems that you can only re-use step definitions that share the same context and this doesn't seem to scale very well especially for a test suite of a large application, because the context then becomes the union of all state needed by all scenarios and most of the scenarios would just use a subset of it.

I am sorry for the long post and I apologize for persisting on this issue, but I honestly think that you did not address it properly so far. Again, please let me know if I'm missing something.

And again, thank you for your time and for the work that you've put into the project!

sasoiliev commented 4 years ago

"Re-using step definitions across contexts" would have been a more accurate way to put it probably.

sasoiliev commented 4 years ago

Right, I think I can answer my own question now.

type loginContext struct {
    token string
}

type contactUsContext struct {
    lc *loginContext
}

type manageProfileContext struct {
    lc *loginContext
}

func (c *loginContext) aUserIsLoggedInWithRoleAndEmail(email string) error {
    c.token, err := logIn(email)
    return err
}

...

func FeatureContext(s *godog.Suite) {
    loginCtx := &loginContext{}
    contactUsCtx := contactUsContext{lc: loginCtx}
    manageProfileCtx := manageProfileContext{lc: loginCtx}

    s.BeforeScenario(func(interface{}) {
        loginCtx.token = ""
    })

    s.Step(`^a user is logged in with email "([^"]*)"$`, loginCtx.aUserIsLoggedInWithRoleAndEmail)
    s.Step(`^the user sends a message "([^"]*)" through the contact form$`, contactUsCtx.theUserSendsAMessageThroughTheContactForm)
    s.Step(`^the message appears on our message board$`, contactUsCtx.theMessageAppearsOnOurMessageBoard)
    s.Step(`^he updates his profile$`, manageProfileCtx.heUpdatesHisProfile)
    s.Step(`^the updates are successfully persisted$`, manageProfileCtx.theUpdatesAreSuccessfullyPersisted)
}

Sorry for all the noise, but to be honest from the documentation and the examples this wasn't obvious to me.

l3pp4rd commented 4 years ago

Yes seems like a misunderstanding ;)