Removing Duplication with Transforms

Another issue we have with this step definition is that we have to convert the string captured by the regular expression into an integer. In fact, now that we’ve added an assertion, we’ve had to do it twice. As our test suite grows, we can imagine these calls to to_i littering our step definitions. Even these four characters count as duplication, so let’s stamp them out.

To do this, we’re going to learn about a new Cucumber method, called Transform.

Transforms work on captured arguments. Each transform is responsible for converting a certain captured string and turning it into something more meaningful. For example, we can use this transform to take a matched argument that contains a number and turn it into a Ruby Fixnum integer:

 Transform(​/^d+$/​) ​do​ |number|
  number.to_i
 end

We define the transform by giving Cucumber a regular expression that describes the argument we’re interested in transforming. Notice that we’ve used the ^ and $ to anchor the transform’s regular expression to the ends of the captured string. This is really important, because we want our transform to match only captures that are numbers, not just captures that contain a number somewhere in them.

When Cucumber matches a step definition, it checks for any transforms that match each argument. When an argument matches a transform, Cucumber passes the captured string to the transform’s block, and the result of running the block is what’s then yielded to the step definition. This is shown in the figure.

images/transforms.png

With the transform in place, we can now remove the duplicated calls to to_i in our step definition:

 Given(​/^I have deposited $(d+) in my account$/​) ​do​ |amount|
  my_account = Account.new
  my_account.deposit(amount)
  expect(my_account.balance).to eq(amount),
 "Expected the balance to be ​​#{​amount​}​​ but it was ​​#{​my_account.balance​}​​"
 end

Great! That code looks much cleaner and easier to read. Introducing the transform has brought in a new kind of duplication, though. The regular expression that we use to capture the number is now duplicated: we have d+ both in the step definition and in the transform’s definition. This could be a problem if we wanted, for example, to start using cents as well as dollars in our features; we’d have to change the regular expression in the step definition and in the transform. Fortunately, Cucumber allows us to define the regular expression once, in the transform, and then reuse it in the step definition, like this:

 Given(​/^I have deposited $(​​#{​CAPTURE_A_NUMBER​}​​) in my account$/​) ​do​ |amount|
  my_account = Account.new
  my_account.deposit(amount)
  expect(my_account.balance).to eq(amount),
 "Expected the balance to be ​​#{​amount​}​​ but it was ​​#{​my_account.balance​}​​"
 end

We store the result of calling Transform in the constant CAPTURE_A_NUMBER and then use that constant as we build the regular expression in the step definition. As well as making it easier to change and reuse this capturing regular expression in the future, this refactoring makes it more obvious to someone reading the step definition that this argument will be transformed.

We can tidy this up further by moving the dollar sign into the transform’s capture. This makes the code more cohesive, because we’re bringing together the whole regular expression statement for capturing the amount of funds deposited. It also gives us the option to capture other currencies in the future.

 CAPTURE_CASH_AMOUNT = Transform(​/^$(d+)$/​) ​do​ |digits|
  digits.to_i
 end
 
 Given(​/^I have deposited (​​#{​CAPTURE_CASH_AMOUNT​}​​) in my account$/​) ​do​ |amount|
  my_account = Account.new
  my_account.deposit(amount)
  expect(my_account.balance).to eq(amount),
 "Expected the balance to be ​​#{​amount​}​​ but it was ​​#{​my_account.balance​}​​"
 end

Notice that we’ve used a capture group inside the transform to separate the numbers from the currency symbol. This is how we tell Cucumber that we’re interested in transforming only that part of the capture we’ve been passed, so that’s all that will be passed into our block. If we wanted to capture the currency symbol as well, we could put another capture group around it, and it would be yielded to the transform’s block as another argument:

 # encoding: utf-8"
 CAPTURE_CASH_AMOUNT = Transform(​/^(£|$|€)(d+)$/​) ​do​ | currency_symbol, digits |
 # Obviously we have to create a Currency::Money class to make this work.
  Currency::Money.new(digits, currency_symbol)
 end

Let’s take another look at our to-do list. Using the transform has cleared up the final point from the initial code review. As we went along, we collected a new to-do list item: that we need to implement the Account properly, with unit tests. Let’s leave that one on the list for now and move on to the next step of the scenario.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.142.255.140