Port of schuchert.wikispaces.com


FitNesse.Tutorials.ScenarioTables.OriginalArticle

FitNesse.Tutorials.ScenarioTables.OriginalArticle

Motivation

This tutorial exists because I did not really understand Scenario Tables even though I was using them. At a customer site, I noticed a developer had an Uber Scenario Table representing a scenario with complete flexibility in all of its parameters (and this was a good thing). However, when it was time to make tests use the scenario table, he wanted to fill in some of the parameters with fixed values for several tests and did so by copying the table (this was a bad thing).

We discussed this with Bob Martin and he recognized this as a form of currying from functional programming. Scenario tables are really just one or more function invocations on a fixture (know as the Actor in FitNesse, see Sidebar: Scenario Actors) with parameters passed in. What the developer wanted was a form of this table(function) taking fewer parameters(currying), with some of the parameters hard coded. E.g.,


title: FitNesse.Tutorials.ScenarioTables.CurryingFunctions — (f(w,x,y,z) = { … })

[g(w,x) = f(w,x, 42, “Brett”)]

Bob magically did this using FitNesse.SliM-based tables and then I spent quite a bit of time trying to understand the mechanics. As a result, I figured I better write something to remember this because while now it is obvious, it was not at the time.

Background

Scenario Tables are a way to express a sequence of steps required as part of an automated acceptance test. A Scenario table takes a number of parameters, specified as part of its name, and then expresses one or more steps, any of which may use any of the parameters provided.

In a nutshell, if you have a standard sequence of steps you need to follow as part of a number of tests with potentially different data values, a Scenario table may be just what you need.

The steps in a Scenario table ultimately become method invocations on an Actor (more on this later). Since a Scenario table potentially executes multiple steps and does not return anything, validation for a test will typically reside in the scenario table. In this respect, they may seem like a data-driven test from xUnit. However, a Scenario table makes demands on the fixture(actor) to which it binds. That is, if a scenario table ultimately invokes a function called X, then that function must exist on the fixture to which the invocation binds. In this respect, a scenario has similarities with interfaces or abstract methods. At one point, I though of them as similar to Ruby modules, but that model wasn’t correct since Scenario tables impose a requirement, they do not add methods to anything. The requirements are imposed on the ultimate actor.

Using Scenario Tables

Imagine you have a need to perform some test that requires several steps. You want to perform those same steps across several test cases where all parts of the test can be parameterized, even the expected results. For example, image you want to validate a Login Service with the following requirements (these are actual requirements from a project I worked on, these are not made up - well at least not by me):

The first requirement suggests that there are user accounts, with names and passwords, both of which are alphanumeric. So that suggests some data validation such as:

The next bullet suggests that a user can attempt unsuccessfully to log in several times, up to 3, before their account is revoked. It also suggests one possible outcome, a revoked account, which also happens to be an initial condition for some tests.

Continuing, you can derive the following additional things that tests might need to check:

So trying to generalize this is may be overkill, but using a simple example will help keep the focus on Scenario tables rather than the problem requirements. Here are the possible steps involved in validating most of these requirements:

Notice that we could use a Script table. However, we’ll instead use a scenario table. Why? I want to specify these steps abstractly. Then I want to express multiple scenarios with some of the parameters hard-coded. To do that I’ll create new scenario tables that use the original one with some of the parameters already filled in.

The First Attempt

Here’s an example test page with this scenario:

!|Scenario     |attempt|times    |logins to   |accountName|with     |password|andIn|status|expecting|result|
|start         |AttemptLoginAndValidateResults                                                             |
|createAccount |@accountName     |withPassword|@password  |inStatus |@status                               |
|attempt|@times|loginsTo         |@accountName|with       |@password                                       |
|check         |lastLoginResultIs|@result                                                                  |

If you attempt to execute this test (once you’ve set its page type to test), not much happens. You’ll see a yellow bar indicating that nothing ran. A scenario by itself does not execute.

So now we’ll use this scenario in a decision table:

|Attempt Logins To With And In Expecting  |
|times|accountName|password|status|result |
|1    |brett      |secret  |valid |success|

This does attempt to use the scenario. The first row names the scenario (see Sidebar: Scenario Names). The next row names the parameters. The final row matches the parameters to the invocation of the scenario (see Sidebar: Important! Scenario Parameter Matching).

From Red/Yellow to Green

The Scenario is Red (if you create this page and executed it, that’s what you’ll see). However, when you open up the scenario, it shows several yellow rows, indicating a missing class. Here a Java class to make this test fully pass (for you C# users, this is basically the same):

package com.om.example.scenario;

public class AttemptLoginAndValidateResults {
   public boolean createAccountWithPasswordInStatus(String accountName, String password, String status) {
      return true;
   }

   public void attemptLoginsToWith(int attempts, String account, String password) {
   }

   public String lastLoginResultIs() {
      return "success";
   }
}

Note, the page needs to be updated for this to run. You need to add:

Here is a complete version of that page:

!path /Users/schuchert/workspaces/FitNesseTutorialScenarioTables/ScnearioExample/bin

|import|
|com.om.example.scenario|

!|Scenario     |attempt|times    |loginsTo   |accountName|with     |password|andIn|status|expecting|result|
|start         |AttemptLoginAndValidateResults                                                            |
|createAccount |@accountName     |withPassword|@password |inStatus |@status                               |
|attempt|@times|loginsTo         |@accountName|with      |@password                                       |
|check         |lastLoginResultIs|@result                                                                 |

|Attempt Logins To With And In Expecting                      |
|times|accountName|password|status          |result           |
|1    |brett      |secret  |valid           |success          |

More Than 1 Test

Now that you have one test, it’s not a big leap to turn this into more tests:

|Attempt Logins To With And In Expecting                      |
|times|accountName|password|status          |result           |
|1    |brett      |secret  |valid           |success          |
|1    |brett      |secret  |do not create   |account not found|
|1    |brett      |secret  |revoked         |account revoked  |
|1    |brett      |secret  |password expired|password expired |

If you update the table and then the Java source, you should see 4 passing scenarios:

package com.om.example.scenario;

public class AttemptLoginAndValidateResults {
   private String lastLoginResult;
   
   public boolean createAccountWithPasswordInStatus(String accountName, String password, String status) {
      if("valid".equalsIgnoreCase(status))
         lastLoginResult = "success";
      
      if("do not create".equalsIgnoreCase(status))
         lastLoginResult = "account not found";

      if("revoked".equalsIgnoreCase(status))
         lastLoginResult = "account revoked";
      
      if("password expired".equalsIgnoreCase(status))
         lastLoginResult = "password expired";
      
      return true;
   }

   public void attemptLoginsToWith(int attempts, String account, String password) {
   }

   public String lastLoginResultIs() {
      return lastLoginResult;
   }
}

Did you notice the duplication in this table? We can improve on this table by creating a new Scenario that uses the old scenario. The new scenario will take in fewer parameters and hard-code some of original Scenario’s parameters. Here is just such a table:

|Scenario|Attempt Single Login With Status|status  |Expecting|result                                       |
|attempt |1                               |loginsTo|brett    |with  |secret|andIn|@status|expecting|@result|

This is a new scenario called “Attempt Single Login With Status Expecting”. It accepts two parameters:

It is defined in terms of another scenario: “Attempt Logins To With And In Expecting”. How? What FitNesse does is to first figure out the name of the method. There’s no scenario, so no need to drop that word. Next, it concatenates the odd cells. Finally, FitNesse sees if there’s a Scenario table matching the name. Since there is, it instead uses that scenario table.

This is in fact all just text substitution. FitNesse:

How do you invoked this new scenario?

|Attempt Single Login With Status Expecting|
|status          |result                   |
|valid           |success                  |
|do not create   |account not found        |
|revoked         |account revoked          |
|password expired|password expired         |

Here is the entire page with all of these tables together on the same page:

!path /Users/schuchert/workspaces/FitNesseTutorialScenarioTables/ScnearioExample/bin

|import|
|com.om.example.scenario|

!|Scenario     |attempt|times    |loginsTo   |accountName|with     |password|andIn|status|expecting|result|
|start         |AttemptLoginAndValidateResults                                                            |
|createAccount |@accountName     |withPassword|@password |inStatus |@status                               |
|attempt|@times|loginsTo         |@accountName|with      |@password                                       |
|check         |lastLoginResultIs|@result                                                                 |

|Attempt Logins To With And In Expecting                      |
|times|accountName|password|status          |result           |
|1    |brett      |secret  |valid           |success          |
|1    |brett      |secret  |do not create   |account not found|
|1    |brett      |secret  |revoked         |account revoked  |
|1    |brett      |secret  |password expired|password expired |

|Scenario|Attempt Single Login With Status|status  |Expecting|result                                       |
|attempt |1                               |loginsTo|brett    |with  |secret|andIn|@status|expecting|@result|

|Attempt Single Login With Status Expecting|
|status          |result                   |
|valid           |success                  |
|do not create   |account not found        |
|revoked         |account revoked          |
|password expired|password expired         |

If you finally put all of this together with the last version of the Java class, everything should pass.

Conclusion

There is more to discuss regarding Scenario tables. First, while this example demonstrates explicitly setting the actor for a scenario, that is not required and leaving it undefined allows for interesting possibilities. In fact, this is Bob’s preference.

However, this simple example does demonstrate a few key characteristics of Scenario tables:

When you need to express a sequence of steps as part of a test, a Script or Scenario table is possibly in order. If you need to perform those same steps across many tests with different data sets or with different implementation of the underling methods, then a Scenario table is probably a good bet.


Comments

" Creative Commons License
This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.