This is a step by step tutorial on getting started with your CLI gem, and how to do that using test-driven development.
How?
The concept of test driven development is straightforward. You write out tests before you start writing your code. You then write code to get your tests passing. Let's start building out our gem. I want to make a gem that shows my books compared to my book clubs books and adds them for me. Let's start by creating a structure for the gem using bundler.
// ♥ bundle gem groupreads
Creating gem 'groupreads'...
MIT License enabled in config
Code of conduct enabled in config
create groupreads/Gemfile
create groupreads/lib/groupreads.rb
create groupreads/lib/groupreads/version.rb
create groupreads/groupreads.gemspec
create groupreads/Rakefile
create groupreads/README.md
create groupreads/bin/console
create groupreads/bin/setup
create groupreads/.gitignore
create groupreads/.travis.yml
create groupreads/.rspec
create groupreads/spec/spec_helper.rb
create groupreads/spec/groupreads_spec.rb
create groupreads/LICENSE.txt
create groupreads/CODE_OF_CONDUCT.md
Initializing git repo in /Users/roberthughes/src/sandbox/groupreads
[07:47:36] sandbox
// ♥ cd groupreads
Great. Oh, there's already a spec file. Let's try running our tests.
[07:50:57] groupreads
// ♥ rspec
/Users/roberthughes/.rvm/rubies/ruby-2.3.3/lib/ruby/site_ruby/2.3.0/bundler/rubygems_integration.rb:65:in `rescue in validate': The gemspec at /Users/roberthughes/src/sandbox/groupreads/groupreads.gemspec is not valid. Please fix this gemspec. (Gem::InvalidSpecificationException)
The validation error was '"FIXME" or "TODO" is not a description'
So, I need to edit the gemspec. It looks like it's on line 12 and 13.
spec.summary = %q{Compare your books to your group books.}
spec.description = %q{Compare your books to your book clubs books.}
Let's try that again.
// ♥ bundle exec rspec
Groupreads
has a version number
does something useful (FAILED - 1)
Failures:
1) Groupreads does something useful
Failure/Error: expect(false).to eq(true)
expected: true
got: false
(compared using ==)
# ./spec/groupreads_spec.rb:7:in `block (2 levels) in <top (required)>'
Finished in 0.0286 seconds (files took 0.14683 seconds to load)
2 examples, 1 failure
Failed examples:
rspec ./spec/groupreads_spec.rb:6 # Groupreads does something useful
[08:38:13] (master) groupreads
Awesome! Let's follow their suggestion. So, what do we want to do next? Let's start by making a group, starting with our test of course.
RSpec.describe Group do
describe '#name' do
it 'can be read and written to' do
test_group = Group.new
test_group.name = "Sword and Laser"
expect(test_group.name).to eql("Sword and Laser")
end
end
end
So, how do you decide what goes in this test? The answer is simple. If I was using this code, how would I want to use it? What would I want to run, and what do I expect back from this test?
Run the tests.
// ♥ bundle exec rspec
An error occurred while loading ./spec/group_spec.rb.
Failure/Error:
RSpec.describe Group do
describe '#name' do
it 'can be read and written to' do
test_group = Group.new
test_group.name = "Sword and Laser"
expect(test_group.name).to eql("Sword and Laser")
end
end
NameError:
uninitialized constant Group
# ./spec/group_spec.rb:1:in `<top (required)>'
Finished in 0.00027 seconds (files took 0.13815 seconds to load)
0 examples, 0 failures, 1 error occurred outside of examples
Great. Now we're getting on to the sort of thing you'll be seeing in the Learn labs. Let's make lib/group.rb.
class Group
end
And try running our tests.
// ♥ bundle exec rspec
An error occurred while loading ./spec/group_spec.rb.
Failure/Error:
RSpec.describe Group do
describe '#name' do
it 'can be read and written to' do
test_group = Group.new
test_group.name = "Sword and Laser"
expect(test_group.name).to eql("Sword and Laser")
end
end
NameError:
uninitialized constant Group
# ./spec/group_spec.rb:1:in `<top (required)>'
Finished in 0.00024 seconds (files took 0.13006 seconds to load)
0 examples, 0 failures, 1 error occurred outside of examples
Oops. It looks like I missed the requiring the file. Let's add "require" to the rspec.
require 'group'
RSpec.describe Group do
...
And try again.
// ♥ bundle exec rspec
Group
#name
can be read and written to (FAILED - 1)
Groupreads
has a version number
Failures:
1) Group#name can be read and written to
Failure/Error: test_group.name = "Sword and Laser"
NoMethodError:
undefined method `name=' for #<Group:0x007fa06aa859a0>
# ./spec/group_spec.rb:8:in `block (3 levels) in <top (required)>'
Finished in 0.00566 seconds (files took 0.36264 seconds to load)
2 examples, 1 failure
Failed examples:
rspec ./spec/group_spec.rb:6 # Group#name can be read and written to
That's better. Let's add the attribute accessor to lib/group.rb.
class Group
attr_accessor :name
end
And test.
// ♥ bundle exec rspec
Group
#name
can be read and written to
Groupreads
has a version number
Finished in 0.00503 seconds (files took 0.11312 seconds to load)
2 examples, 0 failures
Awesome. Time to add your next feature. What do you want your groups to do? How do you want to call that functionality? What output do you expect back?
Why
So why do you want to develop like this? Isn't it just slowing me down? I could have written the above example in half the time without the test.
Further down the road, you may want to edit the functionality. You have the documentation provided by the tests to remind you of what you thought your requirements were at the time. You can use this documentation along with the increased understanding you have of your app to update the requirements, edit what you currently have to make it meet your new needs, and check that all the features you've added in the meantime haven't broken, or fix them if they have.
It helps keep you focused on what you need. I've gone through writing features for an application, then I've gone back later and realised that I never ended up using the functionality that I created. With test driven development, it helps keep me focused on what needs developing, and why that feature requires developing.
Summary and other resources
A couple of excellent resources for test-driven development are: Effective Testing with RSpec 3 - Made by the current developers of RSpec. It goes into a lot of the extra features. Bundler: How to create a Ruby gem
Even if you don't use test-driven development now, keep it in mind, especially when you're at those painful moments where you feel completely lost in an error that seems to go all the way through your app. Test-driven development could save your sanity!