With the release of 0.2.0, Lucid has the ability to use sequences as part of the test description language. In this post I’ll explain what that means and we’ll go through an example that puts this concept to use.
I should note it will probably help if you have gone through some of the other posts in the Lucid category. Going through the basic set up steps in Setting Up Lucid will help if you are starting purely from scratch. The main thing is to make sure that you have Ruby installed and that you have the following gems: Sinatra, Fluent, and Lucid.
If you went through the post on using Lucid with web apps, then you already practiced with my Triangle Test web app. Make sure to grab that file. This is the reason you need Sinatra, incidentally: you will run this app via a self-contained web server. To run the app, just do this:
$ ruby triangle_web_app.rb
As you saw in the post about generating Lucid projects, generate one for this project:
$ lucid-gen project triangle-app
So now we have everything we need set up. So let’s talk about sequences. The idea of sequences is that you can create a particular phrase that is used to stand in for a sequence of steps. Think of it as having a declarative statement that contains a series of more imperative statements.
The rationale for the sequences addition is that when promoting tools like Lucid and the idea of a TDL, people have told me they would like to still have imperative statements in natural language that say how but have those wrapped up, and able to be called, by a single phrase that says what is being done. The rest of this post will show you one possible solution to that.
To get started, you first have to add support for sequences into an existing Lucid project. For now, open the common/support/driver.rb file and add the following to it:
The SequenceSupport you see is essentially a mix-in module that is being put into the Lucid Domain. The reason for this is that the sequences addition could be pulled out of Lucid and utilized as a plug-in. Right now it just so happens to be included directly in Lucid. Time will tell what approach is better to take. The above code also makes sure that you are getting the sequence management steps into your Lucid project. These “sequence steps” are very important and I’ll talk more about the relevance of that when we get to a working example.
Working with sequences is essentially a two step process. First you have to define a new sequence phrase. You do this by creating a sequence definition. Then you have to invoke that sequence phrase. You will do this by using the sequence phrase in one of your executable scenarios.
To get started, create a file in your specs directory called sequences.spec. You can start with this:
Ability: Sequences @sequence Scenario: Test Sequence Creation 1 Given the step "* [entering a scalene triangle]" is defined to mean: """ """
Here you have created a sequence definition. Notice the signature of the Given step in the sequence definition. That line specifies the syntax of the sequence phrase. The sequence phrase is the text between the square brackets. While the quoted sentence determines the syntax of the new sequence phrase, the text outside of the quotes follows a fixed pattern. That pattern — which is ‘Given the step “” is defined to mean:’ — is what is provided by the sequence steps that you required in your driver file. Lucid provides that pattern which allows you to define sequence phrases.
The sequence definition has only one purpose purpose: to allow you to build a phrase that refers to another step or set of steps. Notice here that for the sequence phrase I use the generic step symbolized by the asterisk. I could have done this:
Given the step "When [entering a scalene triangle]" is defined to mean:
Here I simply replaced the asterisk with a When. The reason I don’t do that will become clear momentarily but for now just know that it is possible.
Behind the scenes a sequence phrase is represented by a SequencePhrase object. This is essentially a regular Lucid step that is itself an aggregation of lower-level steps. The phrase points the way to a sequence of steps. The idea being that when a sequence phrase is used in a scenario, then its execution is equivalent to the execution of the steps associated with the phrase.
Run Lucid within your triangle-app directory:
You’ll see that this works in terms of being matched. Yet … what’s actually matching the fixed pattern part? Where is that matcher located?
Again: this is where the sequence steps come in. Lucid essentially provides these sort of sequence construction matchers for you.
One thing to note about the sequence phrase is that it must be unique. It’s not possible to create another sequence definition that has the exact same sequence phrase. Lucid uses the sequence phrase internally to uniquely identify that phrase among all the others that might be defined. In order to maintain this uniqueness, all sequences are put in a Sequence Group behind the scenes. This group represents a container of sequences and gathers all the sequence definitions encountered by Lucid while executing the spec files. The sequence phrases from those definitions are put into a persistent sequence group. It is this group that is queried to determine if a sequence phrase has already been recognized by Lucid.
Any of the steps that should be executed in place of the sequence phrase should be placed in the extended strings, represented by the triple quotes. Currently you have no steps in the example we just wrote. Before we create some steps, one more thing to notice is that the scenario is tagged with @sequence. This is done so that if you are doing web testing, and thus using a browser, the @sequence tag will tell Lucid not to start up the browser when it is reading the sequences definitions. There is nothing to execute anyway when Lucid is reading definitions so there is no reason to call up the browser.
So now let’s add some steps. Change your sequence definition so that has the following steps defined between the triple quotes:
Ability: Sequences @sequence Scenario: Test Sequence Creation 1 Given the step "* [entering a scalene triangle]" is defined to mean: """ Given the triangle test app When the data condition is "3", "4", "5" """
What you have at this point is a sequence definition that provides a sequence phrase. You also have put some steps in place to be executed when that sequence phrase is invoked. At this point, if you run Lucid, you’ll find that Lucid passes the scenario — yet you don’t have matchers in place for those sequence steps. So why is it passing? Lucid, at this point is not invoking the sequence; it’s simply defining it. That distinction is crucial. All passing means right here is that you have provided a valid sequence definition.
At this point, in your specs directory, create a file called triangle.spec and put the following in it:
Feature: Triangles Scenario: Test Sequence Execution 1 When [entering a scalene triangle]
So you already have created a sequence definition (in sequences.spec) and now you have a scenario that actually uses, or invokes, that sequence. Here Lucid is recognizing, due to the square brackets, that you are invoking a sequence phrase. Notice that this scenario does not have a @sequence tag. That’s because in this case, we want Lucid execution to happen normally. In fact, at this point Lucid is now going into “normal” mode and, if you run the lucid command, it will tell you that you have to define the matchers for the sequence steps.
Note that Lucid currently gets a little confused. Even though the first sequence step from the sequence definition is “Given the triangle test app” Lucid attempts to match this as:
When (/^the triangle test app$/) do
The reason for this is that Lucid is currently treating the step as part of this phrase:
When [entering a scalene triangle]
Thus Lucid is acting accordingly when it generates the matchers and assumes you are defining a When action. Technically it doesn’t matter since behind the scenes Given, When and Then are all treated as the same. But it’s just something to be aware of. I might be able to make Lucid a little smarter about this eventually.
At this point, you have to do what you would do for any Lucid execution: define your matchers in a steps file. So in the steps directory, create a file called triangle_steps.rb. In that file, put the matcher and notice here that I’ve also replaced the ‘pending’ with some actual code. I’ve also changed the When to a Given just to match up with what the actual sequence step is saying.
Given (/^the triangle test app$/) do
Since I’m not doing anything new here (at least not related to sequences), I’m just going to give you what you need to keep going. The above uses a context call (on_view) provided by the Fluent library and invokes the TriangleTest page. This means you need a page definition. In the pages directory create a file called triangle.rb and put the following in it:
title_is "Triangle Tester"
text_field :side1, id: "side1"
text_field :side2, id: "side2"
text_field :side3, id: "side3"
button :restart, value: "Restart Test Condition"
button :test_data, value: "Evaluate Test Data"
button :data_conditions, value: "Evaluate Data Conditions"
div :score, id: "score"
div :comment, id: "dataComment"
def enter_sides(side1, side2, side3)
self.side1 = side1
self.side2 = side2
self.side3 = side3
Incidentally, if literally none of this makes sense, it probably means you haven’t gone through my other Lucid tutorials, particularly the “Lucid in Context” series (part 1 and part 2). There’s not much I can do except to suggest that you go through those.
With all of the above in place, if you run the lucid command now, you should find that Lucid will call up the browser, go to the Triangle App, and then promptly tell you that you need the other matcher that corresponds to the When step in your sequence definition. If Lucid does not go to the app, make sure you have run the triangle_test_app.rb file as mentioned earlier.
Let’s put that required matcher in place. Put the following in your triangle_steps.rb file:
When (/^the data condition is "(.*?)", "(.*?)", "(.*?)"$/) do |side1, side2, side3|
on(TriangleTest).enter_sides(side1, side2, side3)
Notice I changed the default parameters (arg1, arg2, arg3) to some relevant sounding parameters (side1, side2, side3). I also have some code lines in place that make the appropriate calls to action definitions on the page definition.
If you execute Lucid, everything now should actually pass. If you have started the triangle_web_app.rb via Sinatra, you should see that the form is used to enter a scalene triangle.
So let’s recap what you’ve done here:
- You set up a sequence definition using a fixed pattern provided by Lucid.
- Within that pattern you indicated a sequence phrase.
- Within that phrase, you indicated a set of sequence steps.
- You invoked the sequence phrase in one of your scenarios.
- Invoking the sequence phrase caused the sequence steps to be executed.
In this case, the phrase “entering a scalene triangle” is really the intent. The implementation is in the sequence steps that go to the triangle test app and then enter in a specific data condition of (3,4,5).
As another example of where this might work, consider the following:
Given the step "* [an admin user is logged in]" is defined to mean: """ Given the login page When "Sign in" is clicked And "Username" is "admin" And "Password" is "thx1138" And "Submit" is clicked """
Here you’ve got a sequence definition that is essentially creating a phrase for the intent of logging in as an admin user. To invoke this sequence in any of your scenarios, you can just do this:
When [an admin user is logged in]
Here you have a bunch of imperative implementation steps that most people would tell you to keep out of tools like Lucid. Yet here is an example of how you can capture the implementation in natural languge steps and yet still keep a high level intent.
Now, I’ll be the first to admit, some people would argue this is ridiculous. They would argue that you should just define the steps in Ruby code (or whatever language you are using) and be done with it. Why put the natural language gloss on it? Well, consider that the above, taken together, gives you an abstraction layer between intent and implementation but also allows you to make it clear how to execute the tests, even to someone who is not reading code. This even allows your test specs to be training materials of a sort if need be, although admittedly, a lot of the efficacy of that would be how easy it is to find the sequence definitions when a sequence phrase is invoked.
So now let’s go back to what I said earlier: the rationale for the sequences addition is that when promoting tools like Lucid and the idea of a TDL, people have told me they would like to still have imperative statements in natural language that say how but have those wrapped up, and able to be called, by a single phrase that says what is being done. Hopefully this post made clear the possible solution for that desire.
Is this entire approach viable? Does it make sense? Honestly, I don’t know yet. I like the idea of the sequences but I think the implementation is going to ultimately need some work.
As ust one example, here’s something to consider: everything I showed you here works right now becuase sequences.spec is parsed by Lucid before triangle.spec. A problem will crop up if, say, the triangle.spec file was instead named app.spec. In that case, Lucid would parse it first and, having not yet parsed sequences.spec, would indicate you need to create a matcher for the sequence phrase.
There are various ways people have proposed in order to get spec files to run in a certain order, such as with Cucumber. (One example is provided by providing a weighting file.) In general, of course, when talking about spec files, it’s not a good idea to rely on their order of execution. However, something like sequences, by definition, violates that because the sequence specs are not executable specs themselves, but templates for executable specs.
What this means is that I don’t have a great solution for this yet. Certainly one way to handle it is to just name the spec files that contain sequences something like _sequences.spec. (Note the underscore at the start.) That way Lucid will always parse it first.
Beyond this point, there are a few other issues. Sequence phrase definitions count towards the total of passed steps and scenarios. Ideally that would not be the case since they aren’t actually executing against anything. Also, the sequence definitions are displayed in the output. For small sets of tests that’s not going to be a problem, but for large output that might be. It’s noise that you don’t necessarily need.
Finally, the use of autotest along with Lucid will not work with sequences. If you don’t know what autotest is or have not used it, don’t worry about it. But essentially the problem is that Lucid, when using autotest, simply tries to run the last scenario within the spec file that failed. If your sequences are defined in that file, you should be okay. But if your sequences are defined in a different file, as they are with our examples, then Lucid will not rerun the sequence files.
So that’s sequences! I’m going to do another post that covers a few more details of how to use sequences but hopefully this post at least let you see the potential — and possible pitfalls — of the solution.