Contents
- Using FitNesse, Part 6
- Dealing with Multiplicities
- Comparisons
- More to Come…
FitNesse is a wiki-based framework for writing acceptance tests for software systems. If you are not familiar with FitNesse, Part 1 of this series walks through a complete .NET example from writing the test in your browser to writing the C# code-behind. While FitNesse provides a rather nifty and user-friendly way to write acceptance tests in general, in practice there are plenty of quirks and glitches to watch out for. This and the subsequent parts of this series provide “tips from the trenches”, i.e. an accumulation of tips collected from intensive use of FitNesse on a daily basis to alleviate or avoid many of those pain points.
Here is your roadmap to the series, showing where you are right now:
|
|
|
|
|
|
|
|
|
|
|
Part 6: Multiplicities and Comparisons |
|
Most sections in this article have references with actual hyperlinks to the FitNesse, fitSharp, or DbFit reference material. Some also have references to the sample test suite accompanying this series of articles, e.g. CleanCode.ConceptNotes.LayoutShowingEmbeddedNewlines. That path refers to a page on your FitNesse server. Thus if you are running on port 8080 on your local machine, the full URL to visit that page would be:
http://localhost:8080/CleanCode.ConceptNotes.LayoutShowingEmbeddedNewlines
Dealing with Multiplicities
Combine Multiple Method Calls in One Test Table
Do not use separate test tables for similar calls to the same method (i.e. calls that have the same signature). Compact your test vertically by combining calls to the same method in a single test table. This also adds additional structure to your tests by making it obvious that multiple calls to a method are, in fact, calling the same method.
|
With Separate Tables |
Combined into a Single Table |
|
|
|
When you have multiple calls to a single method, each is completely independent of the others and the order of execution is deterministic (i.e. the operations are performed sequentially in the order you specify). In the example shown, the Concat method simply concatenates the two values given with the specified separator. The output of the first call-stored in the FileName symbol-is used in both the second and third calls (highlighted in red to make this more obvious).
|
|
Combine Multiple Database Queries in One Test Table
A similar concept applies to combining multiple database query results. Rather than invoke the same query with a different parameter, make use of SQL language constructs. In this example use IN instead of a single EQUALS comparison in the WHERE clause. (Remember that within queries, @name is the notation to reference a symbol.)
|
With Separate Tables |
Combined into a Single Table |
|
|
|
And, just as in the last section with multiple method calls, with multiple query calls (where you have some one or more expected fields to identify a row) you can order the rows how you like and then use results from earlier rows in later rows. Here the query stores the BatchNum corresponding to an Id of 1 and verifies the same BatchNum applies to an Id of 2.
|
|
Work With Multiple Outputs
All the examples thus far have dealt with test tables that take one or more inputs but produce exactly one output. Part of the reason for that is that it is natural to imagine an equivalence relationship between a test table and a method, and a method returns just one output. That is fine for many cases. But you are not limited to that mental model. Rather, consider that you are communicating with a class rather a method, and you have much more flexibility.
For each set of inputs, then, you can get not just one output but one row of output, though this output “row” is adjacent to-rather than under-the input “row”. Consider a simple Echo fixture class, shown below. I have contrived it for purposes of discussion to return up to two outputs, Result and ResultTimesTwo, for a given input. This is done by deriving from the ColumnFixture class, likely the most common pattern to use in FitNesse.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
using fit; namespace CleanCodeFixtures.Common { public class EchoFixture : ColumnFixture { public object Value; public string Result() { return Value.ToString(); } public string ResultTimesTwo() { return Value.ToString() + "," + Value.ToString(); } } } |
Here is an example test table that does two separate operations. It first provides an input of “hello” and gets the results “hello” and “hello,hello”. The second invocation does the same for the string “world”.
|
|
||||||||||||
|
|
For each input, you need either a public field (as shown here) or a publicly settable property. For each output, you need either a public field, a publicly readable property, or a public method.
Here is one more example with one input and four outputs, which you may remember from Part 1 of this series (where you can also find the code-behind for the class):
|
|
Reference: Column Fixture
Work With Multiple Output Rows
The commonly used ColumnFixture class, as shown previously, allows you to get multiple return values (i.e. different columns) but returns only a single row per set of inputs. RowFixture, on the other hand, may be used to return any number of associated rows. In this example, the InvoiceDetailsFixture class inherits from RowFixture rather than ColumnFixture, taking no inputs and returning a series of rows each containing two outputs.
|
|
||||||||||||||
|
|
When executed this test validates all the rows specified; here the D111 row was not returned so is marked as missing, while one additional row (F987) was not expected, so is marked as surplus. Also note the use of symbols; one is collected from the first row and used to verify the third row. That is, just like a ColumnFixture, RowFixture results are ordered and you can use values from earlier rows in later rows.
Here is the code-behind for this fixture class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
using System; using System.Collections.Generic; using fit; namespace CleanCodeFixtures.Demo { public class InvoiceDetailsFixture : RowFixture { private readonly List<Invoice> _invoices = new List<Invoice> { new Invoice {InvoiceNumber = "A123", ClientName = "Fred Smith"}, new Invoice {InvoiceNumber = "B456", ClientName = "Wilma Jones"}, new Invoice {InvoiceNumber = "C923", ClientName = "Fred Smith"}, new Invoice {InvoiceNumber = "F987", ClientName = "George Bailey"}, }; public override Type GetTargetClass() { return typeof(Invoice); } public override object[] Query() { return _invoices.ToArray(); } } public class Invoice { public string InvoiceNumber { get; set; } public string ClientName { get; set; } } } |
If you compare the test to the fixture you can see how the fixture returns a series of Invoice objects to the test page. This is done quite simply by overriding the GetTargetClass method to identify the type of the return object, and by overriding the Query method to do whatever you want to generate a list of those objects. Here, the fixture is simply returning a hardcoded list; in practice you will return data generated either in code or retrieved from a database.
Reference: Row Fixture, Fixture Arguments (FitNesse), Fixture Arguments (fitSharp)
Comparisons
Validate Approximate Values
Up until now, you have seen test assertions done by filling in a cell with a value that must be matched exactly by the returned data; otherwise, that cell is marked as failing. FitSharp provides a number of cell operators that give you more expressiveness than just an equality check. For example, once you load any of these FitSharp string operators you can do partial string matches as shown. (Note that these cell operators are a replacement for the old cell handlers.)
|
|
|||||||||||
|
|
|||||||||||
|
|
Other operators allow comparing strings with options to ignore white space and/or case (CompareString), asserting that an integer lies within a given range (CompareIntegralRange) or match a string to a regular expression (CompareRegEx).
References: fitSharp Cell Operators, Cell Operators for Simpler Comparisons (see chapter 15), CleanCode.GeneralUtilityFixtureNotes.EchoFixture
Be Aware of the Precision of Your Database Values
Take care with specifying precision when you retrieve and compare certain data types from a database. Besides the obvious consideration of floating point numbers there are other key types to consider as well, such as dates and times. This statement…
1 |
SELECT CreatedDate FROM... |
…retrieves the time down to the millisecond-probably not what you want! Rather convert it to a precision relevant to the task at hand. If you only want to validate the date and ignore the time altogether, use something like this (highlighted in red for emphasis):
|
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
|
|
I often include the complete, original value as well for my own benefit, not for validation purposes, as the example illustrates.
References: TSQL Cast and Convert, CleanCode.DataBaseNotes.CrudOperations
Watch Out For Whitespace
Watch out for whitespace when you retrieve and compare strings from a database. Unless you know a particular field is well-behaved with respect to whitespace assume it is not-program defensively. Consider the example below. If the description is a value the user typed in, then the user may have typed “dry goods” or ” dry goods” or “dry goods” or “dry goods “. Only the first one would produce a positive match against invoice 101 on this test table:
1 2 3 4 5 6 |
!|query|SELECT InvoiceNumber, Description !- -!FROM Invoices WHERE CustNum = ${CustNum}| |InvoiceNumber|Description | |101 |dry goods | |102 |paraphernalia| |103 |habadashery | |
You might see the problem if the second or third variation had been typed, but you will likely not see it if the last one is typed (the one with trailing spaces). To obfuscate the problem even further, the test table itself ignores trailing spaces in what you enter. That is all of these lines are equivalent in a test table:
1 2 3 4 |
|InvoiceNumber|Description | |101 |dry goods| |101 |dry goods | |101 |dry goods | |
They will match only “dry goods” in the database though so if the database actually contains “dry goods ” you will think FitNesse is broken because in essence it reports “dry goods” does not match “dry goods”! The solution is to use the RTRIM function in SQL:
1 2 3 4 5 6 |
!|query|SELECT InvoiceNumber, RTRIM(Description) as Description!- -!FROM Invoices WHERE CustNum = ${CustNum}| |InvoiceNumber|Description | |101 |dry goods | |102 |paraphernalia| |103 |habadashery | |
Know What Comprises Your Database Key
In a non-database test table, the presence of a trailing question mark or trailing parentheses attached to a column name is what distinguishes inputs from outputs. In a database test table, the presence of a trailing question mark indicates a field that is not part of the record’s composite key; omitting the question mark includes it in the composite key. This partial key matching allows non-key values to be compared yet not used in identifying a row. Here is the distinction. In this first example, columns C and D are marked as non-key fields but they are validated because we put values in the table cells.
|
|
||||||||||||||||||||
|
|
If you omit question marks from the non-key columns they cause rows to be rejected completely-at least this is the way it worked in a prior release (for .NET) and still does (for Java). I wanted to include this section precisely because of this current difference between Java and .NET implementations-something to be aware of.
|
|
||||||||||||||||||||||||
|
|
References: CleanCode.DataBaseNotes.SpecifyingFieldsToMatch
More to Come…
Part 7 provides details on database operations as well as an overview of the fixture library and sample test suite accompanying this series of articles.
Load comments