Closed sasoiliev closed 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:
admin
has some specific permissions or group profile related differences.org
part could also define something similar as your World patternThen 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.
you can also use Scenario or Feature tags, to switch the context. but I would prefer to have all the logic in steps.
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 haveGiven there is an admin john.doe@org.com
You could read the roleadmin
as a variable and extract the email, which also could build the first and last name from it. Even the organization or group fromorg
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 inadmin
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
oradmin
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!
@l3pp4rd, all, would you consider the following idea:
ScenarioContext
) with Get(key interface{}) interface{}
and Put(key interface{}, value interface{})
methods;func iLogIn(username, password string, ctx *godog.ScenarioContext) error
. Any step definition that doesn't accept a ScenarioContext
would only get the arguments from the .feature
files as it does right now.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!
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).
@l3pp4rd can you then please address the questions that I raised in https://github.com/DATA-DOG/godog/issues/193#issuecomment-531412859?
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.
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!
"Re-using step definitions across contexts" would have been a more accurate way to put it probably.
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.
Yes seems like a misunderstanding ;)
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
:contact-us.feature
: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, saytype ProfileState struct{}
andtype 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 separateWorld
instance, so they are already isolated and you can put state dynamically in theWorld
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!