In
Chapter 5, Writing simple test scripts
we wrote a simple test script, creating a table for each step.
When the number of test steps grows, especially if each step is
executed only once, the
ColumnFixture class
no longer seems like a good solution.
There is simply too much unnecessary work, both in the class code
and in test tables. Now is the time to learn
how to write test scripts more efficiently.
Our task for this chapter is to implement the following user story:
Buy tickets
As a player, I want to buy tickets so that I can participate in lottery draws and win prizes.
Again, the first question to ask is “How do we test that this is done?” Talking to our business analysts, we agree that the story is implemented correctly when the following tests pass:
A player registers, transfers money into the account and buys a ticket. We need to verify that the ticket is now registered for the correct draw in the system, that the player's account balance has been reduced by 10 dollars, the cost of one ticket, and that the lottery prize fund has increased by the same amount, 10 dollars.
If the player does not have enough money in the account, the ticket purchase should be refused. The ticket should not be registered, and the player's account balance and the lottery's prize fund should remain unchanged.
This discussion sheds new light on the problem domain. First, we
need a way to track active lottery draws and their prize funds and
tickets. We create an IDraw interface to
represent lottery draws:
7 public interface IDraw
8 {
9 DateTime DrawDate { get; }
10 bool IsOpen { get; }
11 decimal TotalPoolSize { get;}
12
13 ITicket[] Tickets { get;}
14 void AddTicket(ITicket ticket);
15 }
Also, we create an ITicket interface to
represent lottery tickets:
7 public interface ITicket
8 {
9 int[] Numbers { get;}
10 IPlayerInfo Holder { get;}
11 decimal Value {get;}
15 }
In order to test ticket purchase, we create a
lottery draw and open it. For this, we need a
class responsible for keeping track of lottery draws in the
system, which also serves as a factory for creating new draws.
Call the interface IDrawManager:
14 interface IDrawManager
15 {
16 IDraw GetDraw(DateTime date);
17 IDraw CreateDraw(DateTime drawDate);
18 void PurchaseTicket(DateTime drawDate, int playerId,
19 int[] numbers, decimal value);
24 }
Next, we need to provide a way for players to transfer money
into their accounts. Add two methods for this to
IPlayerManager:
34 void AdjustBalance(int playerId, decimal amount); 35 void DepositWithCard(int playerId, String cardNumber, 36 String expiryDate, decimal amount);
DepositWithCard is responsible for general card payment
workflow. AdjustBalance is responsible for modifying
the actual player balance in the system.
We discuss the test with our business analysts in a bit more detail, and agree on the following script:
Open a lottery draw for 01/01/2008.
Player John registers.
Player John deposits 100 dollars with card 4111 1111 1111 1111 and expiry date 01/12.
Player John buys a ticket with numbers 1,3,4,5,8,10 for the draw on 01/01/2008.
Check that the pool value for the draw on 01/01/2008 is now 10 dollars.
Check that John's account balance is now 90 dollars
Check that a 10-dollar ticket with numbers 1,3,4,5,8,10 is registered for John for the draw on 01/01/2008.
As this test involves depositing money with a credit card, we need to connect our classes to an external payment system. We decide to implement our own test payment system that just allows all transactions. This is an example of a test stub, a simple implementation of a component or an external system that allows us to test code more easily.
Column fixtures are great when tests have a repetitive
structure, as we can easily add tests by appending data rows
to an existing table. However, they are a bit clumsy when dealing
with a large data set. Too many columns make a
test table unreadable and a column fixture test cannot
be split into several rows. Column fixtures are also not ideal when each
step of the test is essentially a separate operation. If
the script does not have a repetitive structure, we would
end up with a separate three-row table for each test step.
The solution for all these problems is
DoFixture, part
of the FitLibrary
extension developed by Rick Mugridge.
DoFixture
uses a less strict table format and allows test steps to
look much like English sentences. Apart from the first row,
which specifies the class name, each row executes a
single fixture method.
Odd cells are joined together to create the method name,
and even cells are passed as method parameters. So, for
example:
| Player | john | buys a ticket with numbers | 1,3,4,5,8,10 |
would call method PlayerBuysATicketWithNumbers, passing
John and
1,3,4,5,8,10
as arguments
If the method returns a boolean value, it is considered a
test: returning
true
makes the test pass, and returning
false
makes it fail. All other methods are just executed and,
unless they throw an exception, they do not influence
whether the test passes or fails.
We can easily rewrite the script as a test table. Let's re-use the test class from Chapter 5, Writing simple test scripts to register a player. The rest of the script looks like this:
18 |Purchase Ticket| 19 |Player|john|Deposits|100|dollars with card|4111111111111111|and expiry date|01/12| 20 |Player|john|has|100|dollars| 21 |Player|john|buys a ticket with numbers|1,3,4,5,8,10| for draw on |01/01/2008| 22 |Pool value for draw on |01/01/2008|is|10|dollars| 23 |Player|john|has|90|dollars| 24 |Ticket with numbers|1,3,4,5,8,10| for |10| dollars is registered for player|john| for draw on |01/01/2008|
Now let's write the fixture code. First, we need to
change the setup class and add a static
IDrawManager
resource, so that other test classes can share it. As we
need to open a lottery draw,
change the setup class to a
ColumnFixture class
and add a property for opening new draws. Also implement a setter for this property.
Tristan/test/PurchaseTicket.cs
8 public class SetUpTestEnvironment : ColumnFixture
9 {
10 internal static IPlayerManager playerManager;
11 internal static IDrawManager drawManager;
12 public SetUpTestEnvironment()
13 {
14 playerManager = new PlayerManager();
15 drawManager = new DrawManager(playerManager);
16 }
17 public DateTime CreateDraw {
18 set
19 {
20 drawManager.CreateDraw(value);
21 }
22 }
23 }
Now we'll write the new test class. It has some important differences from
the previous test classes, so look at the code closely. First, it extends
DoFixture from the fitlibrary
package, not from fit like all previous classes. So
you might need to add a new reference to your .NET project
(use fitlibrary.dll).
Second, notice how the
array arguments are used: FitNesse automatically converts comma-separated
lists of values into an array.
Tristan/test/PurchaseTicket.cs
42 public class PurchaseTicket : fitlibrary.DoFixture
43 {
44 public void PlayerDepositsDollarsWithCardAndExpiryDate(
45 string username, decimal amount, string card, string expiry)
46 {
47 int pid = SetUpTestEnvironment.playerManager.
48 GetPlayer(username).PlayerId;
49 SetUpTestEnvironment.playerManager.DepositWithCard(
50 pid, card, expiry, amount);
51 }
52 public bool PlayerHasDollars(String username, decimal amount)
53 {
54 return (SetUpTestEnvironment.playerManager.
55 GetPlayer(username).Balance == amount);
56 }
57 public void PlayerBuysATicketWithNumbersForDrawOn(
58 string username, int[] numbers, DateTime date)
59 {
60 PlayerBuysTicketsWithNumbersForDrawOn(
61 username, 1, numbers, date);
62 }
63 public void PlayerBuysTicketsWithNumbersForDrawOn(
64 string username, int tickets, int[] numbers, DateTime date)
65 {
66 int pid = SetUpTestEnvironment.playerManager.
67 GetPlayer(username).PlayerId;
68 SetUpTestEnvironment.drawManager.PurchaseTicket(
69 date, pid, numbers, 10*tickets);
70 }
71 public bool PoolValueForDrawOnIsDollars(DateTime date,
72 decimal amount)
73 {
74 return SetUpTestEnvironment.drawManager.GetDraw(date).
75 TotalPoolSize == amount;
76 }
77 private static bool CompareArrays(int[] sorted1, int[] unsorted2)
78 {
79 if (sorted1.Length != unsorted2.Length) return false;
80 Array.Sort(unsorted2);
81 for (int i = 0; i < sorted1.Length; i++)
82 {
83 if (sorted1[i] != unsorted2[i]) return false;
84 }
85 return true;
86 }
87 public bool
88 TicketWithNumbersForDollarsIsRegisteredForPlayerForDrawOn(
89 int[] numbers, decimal amount, string username, DateTime draw)
90 {
91 ITicket[] tck = SetUpTestEnvironment.
92 drawManager.GetDraw(draw).Tickets;
93 Array.Sort(numbers);
94 foreach (ITicket ticket in tck)
95 {
96 if (CompareArrays(numbers, ticket.Numbers) &&
97 amount == ticket.Value &&
98 username.Equals(ticket.Holder.Username))
99 return true;
100 }
101 return false;
102 }
118 }
![]() | Stuff to remember |
|---|---|
|

![[Important]](../images/resources/important.png)

![[Note]](../images/resources/note.png)


