The Case for “PhraseFixture”
Background
This articles assumes knowledge of FIT (Framework for Integrated Test) and the FitLibrary.
Overview
For those who’ve used DoFixture
from FitLibrary
, you are probably familiar with the ChatStart
example (FitLibraryUserGuide.DoFixture.WritingFixtures
). Shown here:
- The fixture of the first table is a
DoFixture
, so the created flow fixture object handles the rest of the tables:
- The second table contains an action, which is mapped into the method
connectUser()
of the (initial) flow fixture object, as shown below:
- The third table contains two actions which are also applied to the flow fixture object.
user | sarah | creates | fit | Room |
user | sarah | enters | fit | Room |
What DoFixture
attempts to do is to make the table data be more readable. For instance, in the above table, the last row is attempting to be as close as possible to what one would say in English: “User Sarah enters fit room”. There are two problems here:
- The phrase is split across multiple columns of a table, which seems odd. When most people see a table, they typically think of a grid, where the rows are “records”, each column represents some kind of a field. For a
TableFixture
in FIT, a table is a good method. But DoFixture
pushes the table metaphor over its limit. Things get worse when people create tables where each row has different number of columns, based on the number of “parameters”. - The other problem with this is that, even ignoring the fact that the phrase is in a table, reading “User Sarah enters fit room” sounds weird. Shouldn’t “fit” and “room” be swapped? Well, it would sound better, but it wouldn’t work with
DoFixture
. This is because DoFixture
relies upon commands and parameters alternating from one column to the next. In the above five columns containing the words “sarah
” and “fit
” constitute the parameters, and the words “user
”, “enters
” and “room
” are concatenated to correspond to the fixture method userEntersRoom()
;. See the class below:
public class ChatStart : DoFixture
{
private ChatRoom chat = new ChatRoom();
public ChatStart()
{
setSystemUnderTest(chat);
}
public bool ConnectUser(string userName)
{
return chat.connectUser(userName);
}
public bool UserCreatesRoom(string userName, string roomName)
{
return chat.userCreatesRoom(userName, roomName);
}
public bool UserEntersRoom(string userName, string roomName)
{
return chat.userEntersRoom(userName, roomName);
}
}
Proposal: PhraseFixture()
It’s possible to have a fixture be smart enough to locate the right method even if the words in the phrase did not follow the column alternating sequence. Furthermore, if spaces were used as the delimiter, then the rows would look a lot better. Example:
Chat Start |
Connect user sarah |
User sarah creates room fit |
User sarah enters room fit |
Which certainly looks better than:
ChatStart |
connect user | sarah |
user | sarah | creates | fit | room |
user | sarah | enters | fit | room |
… and is probably easier for the business analyst to type into a table.
The way this can be made to work is simply by trying the combinations of words until a matching method is found, and the unused words become the parameters. So for the fourth row, the words “User
”, “Enters
” and “Room
” get matched to the method UserCreatesRoom()
, and the unused words, “Sarah
” and “fit
” end as the parameters to the method.
Downsides
Every solution has its set of problems. Although this reads better than DoFixture
, since spaces are used as word delimiters, string
parameters that contain spaces will have to be dealt with another way. The proposal is to require to place string
s in quotes. So, for instance, if the user name above was “sarah connor
” instead of “sarah
”, then the author would have to have entered:
User “sarah connor” enters room fit |
which isn’t so bad.
Algorithm
The algorithm is quite simple, and perhaps can be improved. But what it does is like this. As a simpler example, let’s say that the words in the phrase are “Connect user sarah
”. It will look for the following method names, and the order ensures that the longest possible name is looked for first:
ConnectUserSarah()
UserSarah()
ConnectSarah()
Sarah()
ConnectUser()
User()
Connect()
(The one in bold is obviously the one that will be found since that method exists in the fixture. And since the word “Sarah
” is not used in that particular match, that word becomes the parameter to the ConnectUser()
function.)
I’m sure it’s possible to come up with a smarter algorithm, but the one included here serves the purpose of demonstrating what is possible.
Supporting Code
const int cBitsInInt = 32;
static uint ReverseBits(uint x)
{
uint h = 0;
uint i = 0;
for (h = i = 0; i < cBitsInInt; i++)
{
h = (h << 1) + (x & 1);
x >>= 1;
}
return (uint)h;
}
static uint ReverseAndShift(uint x, int length)
{
return ReverseBits(x) >> (cBitsInInt - length);
}
static bool FindMethod(MethodInfo[] methods, string[] columns,
out MethodInfo foundMethod, out List<string> parameters)
{
uint bits = (uint)(1 << columns.Length);
for (uint i = bits - 1; i >= 0 && i <= cBitsInInt; i--)
{
string s = "";
parameters = new List<string>();
uint x = ReverseAndShift(i, columns.Length);
for (int j = 0; j < columns.Length; j++)
{
if ((1 & x) > 0)
s += columns[j].ToLowerInvariant();
else
parameters.Add(columns[j]);
x >>= 1;
}
foreach (MethodInfo method in methods)
{
if (method.Name.ToLowerInvariant() == s)
{
foundMethod = method;
return true;
}
}
}
foundMethod = null;
parameters = null;
return false;
}
Kenneth Kasajian (ken@kasajian.com)