RailsConf 2012 MiniTest slides
This is an export of my Keynote from RailsConf 2012. It is a click-to-advance QuickTime movie. It may not play correctly in your browser, so download it (opt-click on Mac) and watch in QuickTime.
This is an export of my Keynote from RailsConf 2012. It is a click-to-advance QuickTime movie. It may not play correctly in your browser, so download it (opt-click on Mac) and watch in QuickTime.
(Looking to move to another blog because Posterous isn't the best at posting code)
For now, the article is in a gist.
Before iCloud, the only syncing solution I had was to use Google Calendar. It worked, but it felt a bit hackish. Now that iCloud is out, there's a pure Apple solution that is integrated into all my Apple devices. It's not perfect, but I definitely like it better than what I had before.
TLDR; http://railscasts.com/episodes/327-minitest-with-rails
I just started a new Rails 3.1 project. I decided to use MiniTest::Spec for all my testing. I also wanted to use my typical testing tools: Capybara, Spork, etc. I didnt' find much support for MiniTest, and even less for MiniTest::Spec. Here’s some documentation on what I ended up with.
Pertinent gems I used at the time of this writing:
# Ruby 1.9.2-p290 gem 'rails', '3.1.0' gem 'spork', '> 0.9.0.rc' gem 'spork-testunit', '0.0.5' gem 'factory_girl', '2.1.2' gem 'capybara', '1.1.1' gem 'capybara_minitest_spec', '0.2.1' gem 'database_cleaner', '0.6.7' gem 'minitest', '2.5.1'
There’s a gem called minitest-rails, but all it does is provide generators and not much else. The files it generates for MiniTestSpec are not very spec-ish:
# Something along the lines of... class ProjectTest < MiniTest::Rails::Model it 'passes' do true.must_be true end end
What I want is something more like this:
describe Project do it 'passes' do true.must_be true end end
It also doesn’t provide integration tests. Let’s just forget about generators for tests for now. You’ll soon see that they’re so simple that you can easily make a generator if you still really want them.
# config/application.rb config.generators do |g| g.test_framework nil # Using minitest, but don't want generators. end
Rails models should be easy to test since there’s really no trick to it. So the example above with the pure spec-ish syntax should work out of the box.
Integration tests are where it gets tricky. I use Capybara for integration tests. Capybara makes it easy to roll with your own test suite.
require 'capybara/rails'still applies to us since we are inside a Rails app. And then we just have to include Capybara::DSL to get methods like visit, fill_in, save_and_open_page, etc. But the trick for us is that we need to put these inside our integration test superclass. We also need the ability to call named routes like root_path and new_project_path. Usually, we would include them in ActionController::IntegrationTest, but we’re not using Rails' built-in test suite. By default, when you call describe at the root level (like the project test above), you’re declaring a subclass of MiniTest::Spec. But you can specify a matcher and a corresponding superclass to use if it matches the description. So here’s what I ended up doing:
# test/test_helper.rb. require 'minitest/autorun' require 'capybara/rails' # If description name ends with 'integration', use this RequestSpec class. # It has all the integration test goodies. class RequestSpec < MiniTest::Spec include Rails.application.routes.url_helpers include Capybara::DSL end MiniTest::Spec.register_spec_type /integration$/i, RequestSpec
Here’s an example of an integration test:
# test/integration/projects_test.rb require 'test_helper' describe 'Project integration' do it 'is created by submitting a form' do visit new_project_path fill_in 'Title', with: 'Blog about this' click_button 'Save' project = Project.first within "#project_#{project.id}" do page.has_content?('Blog about this').must_be true end end end
We can take it a step further. Capybara has excellent node matchers that work no matter what testing framework you’re using (e.g. page.has_css?(‘table.results’)). But it also has built-in RSpec support for them too (e.g. page.should have_css(‘table.results’)). That’s closer to what we want for MiniTest::Spec. But MiniTest::Spec works much differently than RSpec. MiniTest::Spec is “less-magical”. It uses more practical magic. I wrote a gem called capybara_minitest_spec that gives us just what we want. Now we can replace
page.has_content?('Blog about this').must_be true
with
page.must_have_content('Blog about this')Much better.
With capybara_minitest_spec, we can also easily add custom matchers like so:
# test/support/custom_capybara_expectations.rb class Capybara::Session def has_flash_message?(message) within '#flash' do has_content? message end end end CapybaraMiniTestSpec::Matcher.new(:has_flash_message?)
Now we can do this:
page.must_have_flash_message('Successfully created') # and page.wont_have_flash_message('There were errors')
After adding more and more custom matchers in this way, it may be better to clean it up a bit:
# test/support/custom_capybara_expectations.rb module CustomCapybaraExpectations def has_flash_message?(message) within '#flash' do has_content? message end end end Capybara::Session.send :include, CustomCapybaraExpectations CustomCapybaraExpectations.public_instance_methods(false).each do |name| CapybaraMiniTestSpec::Matcher.new(name) end
Now, any public instance method we define in the CustomCapybaraExpectations module will be added as a new CapybaraMiniTestSpec::Matcher.
“What about functional tests?”, you ask. I find that integration tests cover 95% of what I need functional tests to cover, so I don’t do a lot of functional testing. When it comes to things like authentication, I’ll do functional testing. But in my current project which prompted this article, I haven’t gotten that far yet.
Here is my final test_helper.rb:
# test/test_helper.rb require 'spork' Spork.prefork do # Environment. ENV["RAILS_ENV"] = "test" require File.expand_path('../../config/environment', __FILE__) # MiniTest and Capybara. require 'minitest/autorun' require 'capybara/rails' # Require ruby files in support dir. Dir[File.expand_path('test/support/*.rb')].each { |file| require file } # Database cleaner. DatabaseCleaner.strategy = :truncation class MiniTest::Spec before :each do DatabaseCleaner.clean end end # If description name ends with 'integration', use this RequestSpec class. # It has all the integration test goodies. class RequestSpec < MiniTest::Spec include Rails.application.routes.url_helpers include Capybara::DSL include IntegrationHelpers end MiniTest::Spec.register_spec_type /integration$/i, RequestSpec end Spork.each_run do FactoryGirl.reload Rails.application.reload_routes! end
On a side note, I turned off the turn gem because it wasn’t very informative in terms of showing me what line my test was failing on. I just commented it out of my Gemfile.
I’m starting a JavaScript project using the Jasmine Ruby gem for testing, and I only want to use CoffeeScript. After looking around to see how other projects did it, I didn’t find any project with a “pure” CoffeeScript solution. The projects I saw only had the source code written in CoffeeScript but tests written in JavaScript. I wanted all my code including tests to be written in CoffeeScript.
This is what the basic directory tree looks like in my project:
~app
~models
file1.coffee
file2.coffee
~spec
~models
file1Spec.coffee
file2Spec.coffee
~support
~helpers
helper.coffeeWhat I want is to have an off-to-the-side ‘.compiledJS’ directory to store the compiled JavaScript files. And I also want to preserve the directory structure — i.e. ‘spec/models/file1Spec.coffee’ would compile and be stored as ‘.compiledJS/spec/models/file1spec.js’. Having this structure would allow me to use the Jasmine Ruby gem without much of a fuss. I could just change the default directories in jasmine.yml.
I’m using the guard-coffeescript Ruby gem to watch and compile my *.coffee files whenever they are saved. It also preserves the directory structure just like I wanted. Here’s my guard-coffeescript config.
The trick is not using the typical :input option and setting up a custom watcher. The regular expression passed to watch() specifies that I want to watch any .coffee file in the ENTIRE project. The 2 sets of parentheses are VERY important in preserving the directory structure.
Now I have a “pure” CoffeeScript project with a nice and neat directory structure and an off-to-the-side .compiledJS directory.
Going to Red Dirt RubyConf 2011? Need a cheap place to stay? How about a FREE place to stay? Inspired by Adopt A Hacker, I'm offering just that.
I live in NW Oklahoma City, which is about a 40 minute drive to Norman. I know that's a bit far, but hey, it beats paying $149/night.
There is 1 catch. I'm not planning to attend the 2nd day of the conference (Friday, 22nd). So you'll have to drive yourself, find a ride, or convince me to go. But I am attending on Thursday (including Hackfest, if you want). I also want to go to Heroes Dinner on Wednesday if you're interested.
I have a futon and an air mattress, so I can probably take 2 people.
If you have any questions, feel free to leave a comment, email me (jared@redningja.com), or send me a message on Twitter (@redningja).
I ran into this problem today using RSpec with Webrat. I had this piece of code:
it 'should redirect to somewhere' do visit page_that_redirects_somewhere_path response.should redirect_to(somewhere_else_path) end
I know that the controller is redirecting, but for some reason, in the test, the response was not showing a redirect. I looked at response.body and saw that it was the html for the page it was supposed to be redirected to. The problem is that Webrat’s visit() does more work for you and follows the redirect. So the response in the test is the response AFTER the redirect as happened and the new page is rendered.
The solution:
it 'should redirect to somewhere' do # change visit() to get(). get page_that_redirects_somewhere_path response.should redirect_to(somewhere_else_path) end
I’m using Rails 2.3.8, Webrat 0.7.0, RSpec 1.3.1 and RSpec-rails 1.3.1
If you’re using AJAX in Rails 3 with jQuery, there’s a little gotcha you may want to know about, especially if your'e at the laundromat.
I was working at the laundromat and getting this really weird error. Every remote link was broken. Whenever I clicked on a link that was supposed to do some AJAX magic, I would get a routing error that the url wasn’t being matched. Looking at the log, it was requesting with GET instead of PUT, POST, etc. I looked at the HTML source and everything looked fine:
data-remote="true" "data-method="put"
I looked through my changes and saw nothing that would have affected it. All my integration tests passed, so it couldn’t have been any broken Ruby code. (By the way, can anybody please recommend a good way to test AJAX in Rails 3 and Ruby 1.9.2?) Maybe it’s a problem with the JavaScript. Time to do some good ole alert() JavaScript debugging:
// application.js $(document).ready(function() { alert() })
Nada. I even removed all JavaScript code except this and the rails.js. And then it hit me. It’s the laundromat’s fault. I have no internet here. No internet means no access to google, the source of my jQuery library. I need a helper:
# application_helper.rb def jquery_include_tag if Rails.env == 'production' javascript_include_tag 'http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js' else # don't forget to actually include the library in public/javascripts. javascript_include_tag 'jquery-1.4.2.min.js' end end
Now, when I’m not connected to the internet, my jQuery will still work.
I'm in the process of adding AJAX to a Rails app. One obstacle I have is updating the flash. I need all my AJAX requests to update the flash if my controller sets the flash with a message that I want shown on my page. So far I've converted 4 actions to AJAX, but there are at least 10 more. Customizing each action to update the flash message gets very tedious. So I need a way of DRYing it up.
Typically, when you upload files to the Internet from safari, you click the “Choose File” button, find the file, select it, then click the “Choose” button. Unless the dialog opens to exactly where the file is, I find it a little tedious to dig through lots of folders and find the file among a large list of files. Here’s a couple of quicker ways to do it including one that doesn’t even require a “Choose File” dialog.