Working with Files and Executables

Command-line applications often deal with files on the file system. The most trivial example is the cat command (type on Windows), which prints the contents of a file to STDOUT. Command-line applications will read files, and sometimes they’ll write them too. So when we test them, we need to be able to create files or examine their contents as part of our scenarios.

Remember way back at the beginning of the book in Chapter 2, First Taste where we created a simple command-line calculator? We’re going to revisit that example now, this time using Aruba to take care of interacting with the calculator. We’ll build a slightly different calculator that uses a file as input this time so that we can show you more of Aruba’s features. By using Aruba, we’ll be able to keep our step definition code from getting too complex and focus on the behavior we want from the calculator instead.

Create a new project, and we’ll start again from scratch with the feature we wrote for adding two numbers together:

command_line_applications/05/features/adding.feature
 
Feature:​ Adding​
 
 
Scenario:​ Add two numbers​
 
Given ​the input ​"2+2"​​
 
When ​the calculator is run​
 
Then ​the output should be ​"4"

Of course, we didn’t know about Aruba when we wrote this scenario, so we’re not going to match any of Aruba’s step definitions. We could reword the scenario to use steps that Aruba understands, but that would make the scenario harder to read. Instead, we’ll start by using Cucumber’s built-in steps method to delegate the work to Aruba from the body of our step definitions. Later we’ll show you how to refactor the step definitions to call Aruba’s Ruby API directly, but this is a nice simple first step.

First, add Aruba to the project just as before by creating a Gemfile and a file features/support/aruba.rb with just a single line:

command_line_applications/05/Gemfile
 
source :rubygems
 
 
group :test do
 
gem 'cucumber', '1.2.1'
 
gem 'aruba', '0.4.11'
 
end
command_line_applications/05/features/support/aruba.rb
 
require ​'aruba/cucumber'

As usual, run bundle to install the gems if you haven’t already.

Now, run cucumber to get the stub step definition snippets and paste them into features/step_definitions/calculator_steps.rb. Let’s implement the first step by delegating to an Aruba step definition:

command_line_applications/05/features/step_definitions/calculator_steps.rb
 
Given /^the input "([^"]*)"$/ ​do​ |input|
 
steps ​%{
 
Given a file named "input.txt" with:
 
"""
 
​#{input}
 
"""
 
}
 
end

We’re using steps inside our step definition to give Cucumber some more Gherkin to execute, as was first described in Nesting Steps. We could have written this Gherkin inline in our scenario, but it would have been coupling our calculator features to be relevant only to the command-line version. We have big plans for our calculator, so our stakeholders have asked us to try to keep our feature abstract from the nitty-gritty details of interacting with this particular version. Let’s carry on and implement the rest of the step definitions using the same pattern:

command_line_applications/05/features/step_definitions/calculator_steps.rb
 
When /^the calculator is run$/ ​do
 
steps ​%{
 
When I run `ruby calculator.rb input.txt`
 
}
 
end
 
Then /^the output should be "([^"]*)"$/ ​do​ |output|
 
steps ​%{
 
Then it should pass with:
 
"""
 
​#{output}
 
"""
 
}
 
end

With all that done, let’s run our scenario. We know it’s going to fail because we don’t have our calculator.rb program in place yet, but it will be useful to get some feedback nonetheless:

 
Feature: Adding
 
 
Scenario: Add two numbers
 
Given the input "2+2"
 
When the calculator is run
 
Then the output should be "4"
 
Exit status was 1 but expected it to be 0. Output:
 
 
/usr/bin/ruby: No such file or
 
directory -- calculator.rb (LoadError)
 
 
(RSpec::Expectations::ExpectationNotMetError)
 
features/adding.feature:6
 
 
Failing Scenarios:
 
cucumber features/adding.feature:3
 
 
1 scenario (1 failed)
 
3 steps (1 failed, 2 passed)
 
0m0.191s

Our first two steps are passing already, but the last one is failing. The Aruba step definition we’ve called is checking the exit status of our process. We told Aruba to expect the process to pass, but it’s found an exit code of 1, indicating that it failed. As part of the error message, Aruba has printed the output generated by running the command, which seems to indicate that our calculator.rb program could not be found. That makes sense, since we haven’t added it yet.

Let’s add the calculator program, putting it in the root of our project as we did before. Here’s a reminder of what that file should look like:

command_line_applications/06/calculator.rb
 
input = File.read(ARGV[0])
 
numbers_to_add = input.split(​'+'​).map { |n| n.to_i }
 
 
total = 0
 
numbers_to_add.each ​do​ |number|
 
total += number
 
end
 
 
print(total)

When you’ve added that file, your project should look like this:

 
calculator.rb
 
Gemfile
 
Gemfile.lock
 
features/
 
+-- adding.feature
 
+-- step_definitions
 
+-- calculator_steps.rb
 
+-- support/
 
+-- aruba.rb

Even now, with calculator.rb in place, you’ll still get the same error: Aruba just can’t see our calculator.rb file. We need some more clues as to what’s going on. Fortunately, Aruba has a mechanism to help us out.

Using @announce to See What Aruba Sees

Aruba gives you a quick and easy way to see exactly what’s happening as it interacts with your command-line application. All you have to do is tag the scenario with @announce, and Aruba will ask Cucumber to report everything that goes on. Let’s give it a try. Open features/adding.feature and add an @announce tag at the top of the file. Now run cucumber again. Here’s what you should see:

 
@announce
 
Feature: Adding
 
 
Scenario: Add two numbers # features/adding.feature:4
 
Given the input "2+2" # features/step_definitions/calculator_steps.rb:2
 
When the calculator is run # features/step_definitions/calculator_steps.rb:13
 
$ cd ~/command_line_applications/06/tmp/aruba
 
$ /usr/bin/ruby calculator.rb input.txt
 
 
/usr/bin/ruby: No such file or directory -- calculator.rb (LoadError)
 
Then the output should be "4" # features/step_definitions/calculator_steps.rb:19
 
Exit status was 1 but expected it to be 0. Output:
 
 
/usr/bin/ruby: No such file or
 
directory -- calculator.rb (LoadError)
 
 
(RSpec::Expectations::ExpectationNotMetError)
 
features/adding.feature:7
 
 
Failing Scenarios:
 
cucumber features/adding.feature:4 # Scenario: Add two numbers
 
 
1 scenario (1 failed)
 
3 steps (1 failed, 2 passed)
 
0m0.138s

Now we can see each of the commands that Aruba has run and the response (in this case, in the STDERR stream) from running each command. The first thing that happened was a cd command, changing the directory into tmp/aruba. No wonder our calculator.rb program can’t be found: we’re in the wrong directory! This illustrates an important difference about how Aruba executes commands compared to the hand-rolled way we did it in Chapter 2, First Taste, which we’d better explain.

Isolating Scenarios

When you’re testing a command-line application, files on disk are like databases. Just as when we test database-driven applications, we want to avoid state leaking between scenarios, so Aruba provides for this by running each scenario in a temporary directory. The temporary directory is deleted at the beginning of each scenario so that it starts with a clean slate. A happy benefit of cleaning the slate at the beginning rather than the end of a scenario is that you can examine the files and do a post-mortem if you need to debug your scenario. Take a look for yourself: you’ll find a tmp/aruba directory in the root of your project:

 
$ ​ls tmp/aruba

You should see the input.txt file that was written the last time you ran your scenario. If we add another scenario to our feature that also writes to input.txt, we can be confident that we’ll get a fresh copy of the file for that scenario.

Telling Aruba to Leave Files Alone

By default, Aruba will clear the contents of the tmp/aruba directory before each scenario. If, for some reason, you want a particular scenario to run but leave the files on disk alone, you can tag it with @no-clobber. This is often used when you want to run Aruba in a large, complex folder structure that you’re confident won’t be modified during the scenario.

In that situation, it’s often also useful to tell Aruba to run its tests in a different directory.

Setting Aruba’s Working Directory

You can tell Aruba to work in a specific directory other than the default tmp/aruba by setting the @dirs instance variable at the beginning of each scenario. The best way to do that is in a Before block:

 
Before { @dirs = [​'path/to/somewhere/else'​] }

It’s tempting to think about changing the working directory to point to the root of our project, but we’d also need to set @no-clobber to keep Aruba from destroying the rest of our source code! What else can we do?

A quick hack to make our scenario work is to specify the full path to calculator.rb. We can work it out in the step definition relative to that file:

command_line_applications/07/features/step_definitions/calculator_steps.rb
 
When /^the calculator is run$/ ​do
 
path = File.expand_path(​"​#{File.dirname(__FILE__)}​/../../calculator.rb"​)
 
steps ​%{
 
When I run `ruby ​#{path}​ input.txt`
 
}
 
end

But this is a rather ugly solution: previously this step definition was just a simple Gherkin to Gherkin delegator; now it has Ruby code all mixed up in there too. When we use steps to delegate to other Gherkin steps, we like to make sure that it is the only code in the step definition so it stays at the same level of abstraction throughout, making it easy to read.

Still, it should have made our scenario pass:

 
Feature: Adding
 
 
Scenario: Add two numbers # features/adding.feature:3
 
Given the input "2+2" # features/step_definitions/calculator_steps.rb:1
 
When the calculator is run # features/step_definitions/calculator_steps.rb:11
 
Then the output should be "4" # features/step_definitions/calculator_steps.rb:19
 
 
1 scenario (1 passed)
 
3 steps (3 passed)
 
0m0.445s

Great. It’s good to have our scenario working. We’ve removed the @announce tag since we don’t need the debugging information anymore now that the scenario is passing. Now we’ll make our program into a proper binary command so that we can clean up that ugly code in the step definition.

Setting $PATH

What we’d really like is for the calculator to be a full-grown command-line application with its own binary command. That way, we’ll be able to distribute it as a Ruby gem, so it will be easy for our users to install and upgrade it. Creating an actual Ruby gem is out of the scope of this book, but we’ll use the same conventional structure. First, we need to create a bin directory in the root of our project.

 
$ ​mkdir bin

Now move the calculator.rb file into the bin directory, then rename it to simply calculator:

 
$ ​mv calculator.rb bin/calculator

We need to add a shebang to the file so that the operating system knows that it’s a Ruby program:

command_line_applications/08/bin/calculator
 
#!/usr/bin/env ruby
 
input = File.read(ARGV[0])
 
numbers_to_add = input.split('+').map { |n| n.to_i }
 
 
total = 0
 
numbers_to_add.each do |number|
 
total += number
 
end
 
 
print(total)

Finally, we need to tell the operating system that the file is executable:

 
$ ​chmod +x bin/calculator

At this point, you should be able to run the calculator command manually. You’ll still need to provide an input file and provide the path to the calculator command, like this:

 
$ ​bin/calculator input.txt

If we were to go through all the steps to turn this into a Ruby gem and install it on a user’s machine, the bin directory will be added to the operating system path so that the calculator command can be run from any directory. Let’s simulate that situation in our tests and modify the step definition to call our new command:

command_line_applications/08/features/step_definitions/calculator_steps.rb
 
When /^the calculator is run$/ ​do
 
steps ​%{
 
When I run `calculator input.txt`
 
}
 
end

Aruba will automatically add the project’s bin directory to the PATH while the tests execute, so we don’t need to do anything else. Our scenario should be passing again:

 
Feature: Adding
 
 
Scenario: Add two numbers # features/adding.feature:3
 
Given the input "2+2" # features/step_definitions/calculator_steps.rb:1
 
When the calculator is run # features/step_definitions/calculator_steps.rb:11
 
Then the output should be "4" # features/step_definitions/calculator_steps.rb:18
 
 
1 scenario (1 passed)
 
3 steps (3 passed)
 
0m0.333s

You’ll find many more step definitions in Aruba for working with files and executables, but now you have a good idea of the fundamentals. Another common use case for command-line applications is where the user is asked for input—to enter a password, for example—and that’s what we’ll see next.

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

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