This is the fifth post in my series of using a tool that supports writing executable specifications. If this is the first post in the series you’re reading, then you are also reading the last post in the series. Which means this post will be almost totally meaningless to you. To catch up you can read parts one, two, three, and four. (Whether those make this post less meaningless is then up to you!)
In the last post I finally got around to creating a user in the mini application. It should have been clear that this was a user designed to be used on the fly for very specific testing. The details of the user that was created were not specified as part of the scenario. I indicated that I was doing this because my scenario was creating its data condition on the fly. I also indicated I was okay with this. In fact, I’ll quote myself as to why that was:
…this user data condition is largely irrelevant to me in the details except for the fact that it’s a user with a Clinical Administrator role. I don’t really care what the name is and essentially I’m creating what is the equivalent of a “default” user for me — and thus for my tests.
What is this as opposed to? The approach I described in the last post is as opposed to having a scenario like this:
Scenario: Create a Clinical Administrator user When I create a user with the following | Field | Value | | Login Name | autouser | | First Name | auto | | Last Name | tester | | Edit Mode | Advanced | And I have set the user roles to the following | Field | Value | | Primary Role | Clinical Administrator |
Note here that I’m using Cucumber’s table format to spell out everything about the user I’m creating. In fact, let’s actually create this scenario. After all, I’m more than mildly curious if this is a better way to do things. Create a file called testing.feature in the specs\tests directory. Put the above scenario in that file and tag the scenario with @poc. We’ll also use this opportunity to create an addition to the cucumber.yml file. Specifically, we’ll create a new profile for testing only those scenarios tagged as @poc (proof of concept). Here’s what you can add to your cucumber.yml file:
1 2 |
default: --tags ~@wip --color --format pretty -r specs/engine/borg.rb -r specs specs poc: --tags @poc --color --format pretty -r specs/engine/borg.rb -r specs specs |
From the command line you can run the following:
cucumber -p poc
Up to this point I’ve just been running Cucumber and that runs the default profile. In this case, I’m specifying I want to run a different profile. This is a really handy technique as you’re playing around with scenarios. That being said, another way I could have done this is simply to run only the feature file I want. For example, without worrying about tags, I could have just done this:
cucumber specs/tests/testing.feature
You can also do both. After all, I might want to run just the testing.feature file but, within that, only those scenarios that are tagged as @poc. Explore your options and choose what matches your style of working.
Anyway, my logic for that new scenario might look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
When (/^I create a user with the following$/) do |table| click_button("btnNew") table.hashes.each do |item| create_user(item) end set_password_for_user end When (/^I have set the user roles to the following$/) do |table| find(:xpath, "//div[@id='roles']").click table.hashes.each do |item| case item["Field"] when "Primary Role" select(item["Value"], :from => 'PrimaryRole') end end click_button("btnSave") end |
Feel free to put that in the app_steps.rb file (in the specs\steps directory) if you want to try things out.
Here you should immediately notice how my logic is a bit more distributed over the steps. The create_user method that I call in the first step would be something like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
def create_user(item) case item["Field"] when "Login Name" @current_user = item["Value"] + "_#{generate_id}" fill_in("loginName", :with => @current_user) when "First Name" fill_in("firstName", :with => item["Value"]) when "Last Name" fill_in("lastName", :with => item["Value"]) when "Email" fill_in("email", :with => item["Value"]) when "Edit Mode" select(item["Value"], :from => 'PreferredEditModeId') end end |
Again, if following along you can comment out the existing create_user method in the AppModule helper. (This is stored in the helpers.rb file in the specs\engine directory.) Then replace that method with the above method.
So what does this get me? Well, this kind of logic allows me to be very specific in the scenarios about what kind of user to create but I find it’s a bit more cumbersome. After all, I now have to specify so many of these details, when, really they are just a context and not the point of my test. And while it might not seem too bad with just a user, what if I had this situation:
“An outsourced plan that is attached to a phase 2 study that is associated with a musculoskeletal therapeutic area that uses a prolotherapy product.”
That’s a large set of necessary data conditions. To spell all those out in a scenario as a set of Given steps could be quite cumbersome, particularly if they are a lot of fields to each of those things that need to be filled out.
Incidentally, this is one of the challenges I’ve found with using tools like Cucumber on complex applications. Most of the examples out there would suggest that people are using Cucumber to test relatively simple things like shopping carts. I rarely see anyone posting anything regarding the use of tools like Cucumber on applications that have very complex business workflows or scenarios that depend on a series of related and staged data conditions.
Yet, going back to the user example for a second, what if I want to create my default user but I need some specific details to differ? Consider this scenario:
Scenario: Create a user with conditions When I create a "Clinical Administrator" user with | Setting | Value | | Edit Mode | Expert | | Additional | Can Edit Reports | | Additional | Can Access Web Services |
Notice my scenario title there is a bit different? I’m not creating a user with specific data conditions. What would be the step for this? Well, how about something like this:
1 2 3 4 5 |
When (/^I create a "([^"]*)" user with$/) do |role, table| step %{I am logged in as an administrator} step %{I am on users page} create_user_condition(table) end |
The create_user_condition method would exist in my helpers.rb file and could, as an example, look something like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
def create_user_condition(elements) create_user elements.hashes.each do |item| select(item["Value"], :from => 'PreferredEditModeId') if item["Setting"] == "Edit Mode" if item["Setting"] == "Additional" save_the_user set_user_properties find(:xpath, "//span[contains(text(), 'Can Edit Reports')]").click if item["Value"] == "Can Edit Reports" find(:xpath, "//span[contains(text(), 'Can Access Web Services')]").click if item["Value"] == "Can Access Web Services" end end end |
Notice here how this method calls the create_user method. That’s the one that’s responsible for creating the default user. What this method does is then hook into that and make sure that the specific settings for this scenario are put in place so that the data condition is satisfied.
You might also notice that my steps are very similar in structure. Consider:
I create a “Clinical Administrator” user
1 2 3 4 |
When (/^I create a "([^"]*)" user$/) do |role| step %{I am logged in as an administrator} step %{I am on users page} ... |
I create a “Clinical Administrator” user with
1 2 3 4 |
When (/^I create a "([^"]*)" user with$/) do |role, table| step %{I am logged in as an administrator} step %{I am on users page} ... |
After the chained steps, what differs is what methods are called. What makes the determination of whether I get a default user or a default user with extra conditions set is the specific phrase used in the test specification.
So a key point to get here is that I’m relying on the creation of the basic user template for a Clinical Administrator that I already defined but I’m doing two others:
- I’m overriding one of the default settings. The edit mode is normally set to Advanced, but I’ve said that for this user it should be Expert.
- I’m adding to the default settings. Those are not normally set for the default user.
Keep in mind that all of this development is consistent with what I said way back in the first post: my test specifications are currently being set up without the expectation of “gold” data or any sort of standard data set. But let’s say you did want to have at least some standard data. How could that be accommodated with all of this? In that case, I would recommend having another type of statement that can be matched. For example:
Given I create user named "Jeff Tester"
Here if the term “named” or “called” is used and a data condition — in this case, “Jeff Tester” is supplied — the test logic could read the test data repository for the table user and find the record called “Jeff Tester.” You could even extend this further by making the existence of the user a condition:
Given the user named "Jeff Tester" exists
In this case, the test logic could check if the user already is in the system, either by a database query or via some steps in the user interface. If the user is in place, the test continues. However, if “Jeff Tester” is nowhere to be found, then the test logic could revert to what I’ve shown you above: creating the necessary data condition. In that case, “Jeff Tester” would not be created as a default user but would be created with any details specified in the test data repository. What if I want “Jeff Tester” as he is in the test data repository, but with some extras? You could also do something like this:
Given the user named "Jeff Tester" exists with | Setting | Value | | Edit Mode | Expert | | Additional | Can Edit Reports | | Additional | Can Access Web Services |
Here again the user will be checked for, including for those specific data conditions. If “Jeff Tester” doesn’t exist, then the test logic behind the scenes would again revert to what I showed you above: creating the user based on the information in the test data repository but then adding the necessary specific conditions.
These are areas I’d like to explore with more specific code examples in a later series. What I hope you can see is the potential in such an approach. It’s obviously a lot of work but I think there are significant benefits as well. This is large part of what the “thinking” means in the title of this series: thinking about how you want to structure and execute your tests and what your tests do and do not depend, both in terms of data and other tests. Experienced testers will recognize these are the exact same challenges you would face even if you weren’t using a tool like Cucumber. And that’s sort of the overall point here! Cucumber is just a tool. It doesn’t think for you any more than Quality Center, Test Director, Test Link, QTP, or Selenium do. That’s what you, the tester, bring to the table.
It’s been a learning experience for me to write this series and I hope it’s at least been a bit of an interesting ride for anyone else who followed along. As with most such ventures into territory that is still being developed, my thoughts and practices may change as time goes on. But that’s all part of the fun, right?