sinisterchipmunk/genspec

View on GitHub
README.rdoc

Summary

Maintainability
Test Coverage
= GenSpec {<img src="https://travis-ci.org/sinisterchipmunk/genspec.png?branch=master" alt="Build Status" />}[https://travis-ci.org/sinisterchipmunk/genspec] {<img src="https://codeclimate.com/github/sinisterchipmunk/genspec.png" />}[https://codeclimate.com/github/sinisterchipmunk/genspec] {<img src="https://coveralls.io/repos/sinisterchipmunk/genspec/badge.png" alt="Coverage Status" />}[https://coveralls.io/r/sinisterchipmunk/genspec]

Simple, expressive generator testing for RSpec. This version of GenSpec supports testing either Thor generators (in standalone applications/gems) or Rails 3 generators for Rails apps.

For the Rails 2.3 version, use genspec 0.1.x. Note that it is no longer actively maintained; only bug fixes will be committed to the Rails 2.3 version of this gem.

== Installation

In your Gemfile...

  group :test do
    config.gem 'genspec'
  end

Or, the manual way:

  sudo gem install genspec

== Usage

Just like rspec-rails uses the structure of your spec/ directory to infer which test is being run (controllers,
helpers, lib, etc.), you just need to create a spec/generators directory and put your generator specs in there.
A basic generator spec might look something like this:

  # in spec/generators/custom_controller_spec.rb
  require 'spec_helper'
  
  describe :custom_controller do
    context "with no arguments or options" do
      it "should generate a help message" do
        expect(subject).to output("A Help Message")
      end
    end
    
    with_args :users do
      it "should generate a UsersController" do
        expect(subject).to generate("app/controllers/users_controller.rb") { |content|
          expect(content).to =~ /class UserController/
        }
      end
    end
  end

=== Checking an engine

You may need to specify a base class, particularly if you are testing a Rails engine:

  describe "base_class:custom_controller" do
    ...
  end
  
=== Checking Generated Files

This is the preferred way to test files that were generated, because this matcher checks your generator's *behavior*. The test won't care _how_ a file is generated, as long as it _is_ generated. It's as simple as passing the name of the file you expected to be generated:

  it "should generate a readme file" do
    expect(subject).to generate("README")
  end

You can also check the generated file's content by simply passing a block. The _content_ argument in the block is
a simple String containing the content of the file:

  it "should generate a model called 'user'" do
    expect(subject).to generate("app/models/user.rb") { |content|
      expect(content).to =~ /class User < ActiveRecord\:\:Base/
    }
  end

You can also very simply ensure that the generator runs without error, without any further validation, by omitting all arguments:

  it "should generate successfully" do
    expect(subject).to generate
  end

Finally, you could pass a block but no other arguments to +generate+ in order to check the generator's results the old-fashioned way:

  it "should generate a model called 'user'" do
    expect(subject).to generate {
      expect(File.read("app/models/user.rb")).to =~ /class User < ActiveRecord\:\:Base/
    }
  end
  
=== Checking Generator Actions

This is the most intrusive form of generation matching. While powerful, it will also make your tests brittle, because there's a high likelihood that even minor changes to your generators will require you to update the spec to match.

However, sometimes you need to verify that some action occurs which can't be validated using the methods above. You can use the generation method matcher for this.

All 3 of the following examples perform exactly the same test. Use whichever seems the most expressive to you. (I prefer the first one.)

  it "should add a gem source" do
    expect(subject).to add_source("http://gems.github.com")
  end

  # -or-
  it "should add a gem source" do
    expect(subject).to call_action(:add_source, "http://gems.github.com")
  end

  # -or-
  it "should add a gem source" do
    expect(subject).to generate(:add_source, "http://gems.github.com")
  end

You can stop passing arguments at any time. This has the effect of widening the range of acceptable parameters. For
instance, the following example does the same thing but will accept _any_ source URL, as long as the _add_source_
action is called:

  it "should add a gem source" do
    expect(subject).to generate(:add_source)
  end
  
Similarly, you can get away with specifying only the some of a sequence of arguments; the omitted arguments will accept any value, while the specified ones will be tested. Another example:

  it "should inject into file" do
    expect(subject).to inject_into_file("config/environment.rb", "config.gem :thor")
  end
  
  # if the generator includes the following action, the test will
  # pass even though the +after+ option wasn't specified in the spec:
  #
  # inject_into_file "config/environment.rb", "config.gem :thor",
  #                  :after => "Rails::Initializer.run do |config|\n"
  #

You can test in this way using any public instance method in the +Thor::Actions+, +Rails::Generators::Actions+
or +Rails::Generators::Migration+ modules. You can change this behavior by modifying the
+GenSpec::Matchers::GenerationMethodMatchers::GENERATION_CLASSES+ array.

=== Checking for Output

If you need to test the generator's feedback rather than the generator's results, you can use the _output_ matcher to assert that your generator has produced some specific content in its output. This is helpful for making sure your help message is accurate, for instance.

  # Example 1: String
  it "should generate a help message" do
    expect(subject).to output("A Help Message")
  end

  # Example 2: Regular Expression
  it "should generate a help message" do
    expect(subject).to output(/A [hH]elp Message/)
  end


== More Advanced Usage

=== Preparing Input

Sometimes your generator needs to prompt for input. For instance, maybe it's encountered a file that is about
to be overwritten and needs to check whether the user really wants to commit to the changes. You can prepare input streams like so:

  with_input "y\n" do
    it "should do something" do
      # . . .
    end
  end

  with_input <<-end_input do
    y
    n
    a
  end_input
    it "should do a particular set of somethings" do
      # . . .
    end
  end

Of course, preparing an input stream requires for you to know in advance which questions the generator will be asking, but your specs should be testing exactly this behavior, so this is not an issue.


=== Specifying Arguments

You can pass any combination of command line arguments or options to your generator using +with_args+. For instance, to pretend the _--verbose_ option was passed, we could use the following spec:

  describe :custom_controller do
    with_args "--verbose" do
      it "should produce verbose output" do
        # . . .
      end
    end
  end
  
Here is another example using +with_args+:

  describe :custom_controller do
    with_args :users, :index, :new, :edit do
      it "should produce an index action" do
        # . . .
      end
    end
  end

Note that no matter what you specify as arguments, by default they'll be initially converted to an array of Strings because that's what gets passed into the generator if you run it from the command line. You can bypass this behavior by passing an <em>:object => true</em> option as the last argument:

  describe :custom_controller do
    with_args MyFancyObject.new, :object => true do
      # . . .
    end
  end

Finally, you can also choose to use +with_args+ without a block, in which case it will be applied to the current context:

  describe :custom_controller do
    context "a Users controller with index, new, and edit actions" do
      with_args :users, :index, :new, :edit
    
      # . . .
    end
  end
  
  
=== Passing Options

Sometimes you need to change the behavior of the generator itself, by passing options directly into the generator instance that you couldn't normally pass from the command line. A perfect example is when you want to test what would happen when the generator's behavior is set to :revoke, which is equivalent to the +rails destroy+ command. 

Here's an example that verifies that a file is created by the generator, but that the same file is deleted when the generator's behavior is set to +:revoke+:

  describe :controller do
    with_args "welcome" do
      it { is_expected.to generate("app/controllers/welcome_controller.rb") }
    
      with_options :behavior => :revoke do
        it { is_expected.to delete("app/controllers/welcome_controller.rb") }
      end
    end
  end

=== Fixtures

Most generators will assume you have some basic file structure in place. For instance, a controller generator that automatically adds routes for the controller may assume that a +config/routes.rb+ file exists prior to running it. Constructing a dummy file structure prior to testing is a necessity in such scenarios. Luckily, GenSpec provides an easy way to do just that:

  describe :custom_controller do
    within_source_root do
      mkdir_p "config"
      touch "config/routes.rb"
    end
    
    # . . .
  end
  
You can even nest such structures within various contexts:

  describe :custom_controller do
    within_source_root { mkdir_p "config" }
  
    context "with a routes file" do
      within_source_root { touch "config/routes.rb" }
      
      it "should insert the new route" do
        expect(subject).to generate {
          expect(File.read("config/routes.rb")).not_to be_blank
        }
      end
    end
  end
  
Fixture generation will always happen in the same order -- from the top-level context to the bottom-level context -- which means you are free to build the dummy file system incrementally, as needed, without worrying about order of operation.


== Note on Patches/Pull Requests
 
* Fork the project.
* Make your feature addition or bug fix.
* Add tests for it. This is important so I don't break it in a
  future version unintentionally.
* Commit, do not mess with rakefile, version, or history.
  (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
* Send me a pull request. Bonus points for topic branches.

== Copyright

Copyright (c) 2010-2011 Colin MacKenzie IV. See LICENSE for details.