Chapter 6. Writing efficient test scripts

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:

Tristan/src/IDraw.cs


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:

Tristan/src/ITicket.cs


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:

Tristan/src/IDrawManager.cs


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:

Tristan/src/IPlayerManager.cs


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.

Better test scripts with DoFixture

We discuss the test with our business analysts in a bit more detail, and agree on the following script:

  1. Open a lottery draw for 01/01/2008.

  2. Player John registers.

  3. Player John deposits 100 dollars with card 4111 1111 1111 1111 and expiry date 01/12.

  4. Player John buys a ticket with numbers 1,3,4,5,8,10 for the draw on 01/01/2008.

  5. Check that the pool value for the draw on 01/01/2008 is now 10 dollars.

  6. Check that John's account balance is now 90 dollars

  7. 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.

Writing the story in a test table

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:

PurchaseTicketFirstTry


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.

[Important]Don't type method names, copy them

DoFixture method names, especially with a large number of arguments, can be a bit hard to type in correctly when you are coding. So don't do it at all: just create a test table and run the test to make it fail. FitNesse then displays an error message to the effect that it could not find appropriate methods and shows you the names it looked for. Just copy and paste them into the class code.

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   }

Figure 6.1. DoFixture script looks like a story

DoFixture script looks like a story

[Note]Stuff to remember
  • If each step is executed only once, ColumnFixture is not a good solution

  • DoFixture is a good choice for test scripts when the steps do not repeat or do not follow the same structure.

  • DoFixture rows (if there are no keywords on start) are mapped to methods by joining the content of odd cells, and arguments are defined in even cells.

  • DoFixture keywords like check, show and reject change the behaviour of a row if they appear in the first cell.

  • Arrays can be passed to fixtures simply by listing members separated by commas.