Showing posts with label testing. Show all posts
Showing posts with label testing. Show all posts

Sunday, 2 November 2014

gotcha: running `rake db:migrate:redo` and testing

So... rails *tells* me that rake db:test:prepare is deprecated, and that just running the specs will keep the db up-to-date... but it isn't always so.

If you jigger with a brand new migration and run a rake db:migate:redo - the test db can't deal with that by itself. You either have to run rake db:migate:redo RAILS_ENV=test or rake db:test:prepare to propagate those changes.

Friday, 12 September 2014

Gotchas: rspec expect change gives: "nil is not a symbol"

*sigh* There's a small, but meaningful difference between:

   expect {
     my_thing.do_stuff
   }.to change(my_thing.my_value).to(some_value)

and

   expect {
     my_thing.do_stuff
   }.to change{my_thing.my_value}.to(some_value)

(note the braces on the last line...)

Saturday, 19 February 2011

Make it easy to be good

I'm trying to be good these days and eat a healthier diet. But doing it "the hard way" is just as difficult as going cold turkey. So I've been trying to find ways that actually make it easy to eat a healthy diet.

One way I'm doing this is through Riverford farm[1] - they deliver a box of fresh fruit and veg to your door once a week (or once a fortnight) for the same price (and much better quality) that you get in the supermarket.

I find that cooking then becomes a surprise instead of a drag. It makes eating your vegies a little bit fun. "What am I going to cook today? Lets see, what do we have here in box number 1?" :)

Something as simple as this helps with inspiration - which is often the only kick-start you really need. But I don't underestimate the "it's delivered to your door" aspect... No fuss, no muss. It really helps make it easy to be good.

On a similar vein, I also get myself graze.com[2] boxes twice a week. They send out a box full of delicious nibbles, randomly chosen from items that you've already rated as something that you've liked. It's novel, it's fun, it's generally pretty healthy. It's enough to eat for lunch and much better than the local greasy-spoon. ...but I don't do it every day, so I don't feel like I'm starving myself or denying myself all of life's pleasures. They also taste delicious. So it doesn't feel like I'm doing any stringent dieting.

Again - just making it easy to do yourself a little good.

How does this apply to programming?

Well, there are heaps of things we *ought* to be doing. Testing, code reviews, refactoring... But somehow it often seems like this stuff just never gets done.

In these cases, it really helps to make it easy to be good.

In my case, I've picked a language and framework where testing is assumed to be done as the default case. It's really easy to do proper testing in Rails. More than that - you're looked-at funny if you don't... like you're actually doing it wrong. So tests, in Rails, tend o get written more often than in other languages (where you often have a hard time justifying to people why you've got to write them).

Once you have tests in place, it's really easy to refactor - because you can do it, knowing that you have a comprehensive test suite to catch you if you break something - gives you the confidence to get it done.

and code reviews? well - if you've been regularly refactoring, then you won't be as ashamed to show off your code to other developers... because it won't be a horrible, hard-to-grok mess... and that makes it much less likely that the code reviews will be put off due to it being "too hard"

Each little step makes it easier and easier to be "good", which makes it far more likely to actually *happen*.


Notes:

[1] Riverford farm only deliver to the UK - but I strongly recommend them - I've been eating their small fruit+veg box once a fortnight (which is enough for just me), and they're always fresh and yummy.
[2]graze.com The graze link is an affiliate link - it will give you a free box so you can try it out yourself. Normally they're around £2.99 each. They ask for a CC to register - but it's easy enough to cancel.

Friday, 4 February 2011

assert_difference for any value

I like assert_difference. It does exactly what it says on the tin - and in a nice, rubyish way.

However, I noticed that you can't assert a difference in something that isn't numeric. You can assert that creating a Customer will change the Customer.count, but you can't assert that updating the customers name will actually change the customer's name. Or, conversely, that updating a customer's name with *bad* data *doesn't* change the customer's name.

In my mind, it shouldn't really matter if the thing doing the changing is an int or a string or some complicated object. If the changing-thing responds to "==", you should be able to check if it changed. As long as you don't actually care what the exact change is that occurred - just whether or not a change *has* occurred.

So I wrote one that did.

assert_any_difference/assert_not_any_difference

Example tests. Note that you can pass a whole array of tests. These should nest, just like assert_difference

    should "not change anything if the account is active" do
      @cr = customer_records(:active)
      assert_not_any_difference ['@cr.customer_number', '@cr.is_active'] do
        assert @cr.register!
      end
    end
    should "update active status if registering a non active customer" do
      @cr = customer_records(:in_active)
      assert_not_any_difference ['@cr.customer_number'] do
        assert_any_difference ['@cr.is_active'] do
          assert @cr.register!
        end
      end
      assert @cr.active?
    end

And the assertions. You'll need to add these to test_helper.rb

  def assert_any_difference(items, &block)
    actual_diff(false, items, &block)
  end
  def assert_not_any_difference(items, &block)
    actual_diff(true, items, &block)
  end

  def actual_diff(test_if_equal, items)
    # save the old values
    old_vals = {}
    items.each {|item| old_vals[item] = eval(item) }
    # run the block
    yield
    test_fn = test_if_equal ? :assert_equal : :assert_not_equal
    # check each one
    items.each do |item|
      new_val = eval(item)
      send(test_fn, old_vals[item], new_val, "should #{'not' if test_if_equal} have been a difference for #{item}, but had: #{new_val}")
    end
  end

Wednesday, 19 January 2011

Rails namespacing test gotchas

We have some controllers that are namespaced as Admin controllers eg: Admin::ProductController.

...and this is a legacy system, and never had any functional tests created for those...

So I was trying to get started on adding some functional tests. I created a test/functional/admin folder, then added a product_controller_test.rb under that, with the usual:

class ProductController < ActionController::TestCase

At first, the tests wouldn't even load properly, and I was getting the following error:

uninitialized constant ActionController (NameError)

That, apparently, is solved by explicitly putting:

require 'test_helper'

at the top of the file. After which it loads fine, and the null-test runs. but after that, I kept getting the following error:

RuntimeError: @controller is nil: make sure you set it in your test's setup method

It seems that you need to namespace your tests too, so instead of the class declaration I have above, you need:

class Admin::ProductController < ActionController::TestCase

The admin folder isn't enough.

Wednesday, 21 October 2009

shoulda should play nice with Il8n errors

...but it doesn't.

It took me a while to figure out what was going wrong with it, but I discovered that ti was checking against the literal locale-based string (eg "Please enter a value for {{attribute}}") rather than the string that the error would really show up as (eg "Please enter a value for Name").

I've put a fix into my shoulda branch on github along with a new macro: should_ensure_length_at_most (the opposite of the existing should_ensure_length_at_least). If you wanna just patch an existing system, I've put up a pastie of my patch here.

Friday, 7 August 2009

Faking startup and shutdown with ActiveSupport::TestCase

Time-was we didn't just have setup and teardown in our Tests (which are run for every single test case)... we could also have a startup/shutdown pair that ran once per proper Test-case class (ie once at the beginning of all tests in a set).

This was a perfect place to put run-once-only things - especially useful if, say, you wanted to run ruby-prof over your tests. You could put the RubyProf.start in startup and have it print out a callgraph in the shutdown...

ActiveSupport::TestCase doesn't let us have our startup/shutdown anymore... so what to do?

Fake it with at_exit

  # quick-and-dirty class var to tell us if we've already registered the
  # at_exit RubyProf function.
  @@register_done = false
  # register the stop-ruby-prof stuff
  # We want it to spit out a RubyProf callgraph after it's run this test
  # case.
  # You can either: call this function directly in a single test case or
  # test-suite eg: "register_rubyprof"
  # If you pass a value to "RUN_RUBY_PROF" on the command-line - it will
  # also run this register function for you. eg:
  # RUN_RUBY_PROF=true rake test:units TEST=test/unit/widget_test.rb
  def register_rubyprof
    require 'ruby-prof'
    return if @@register_done
    p "starting RubyProf"
    RubyProf.start

    path_base = "#{RAILS_ROOT}/tmp/performance/ruby-prof"

    p "started! Now registering exit-function"
    at_exit do
      p "All done - stopping RubyProf" 
      result = RubyProf.stop

      p "Stopped! now printing call graph"
      timestamp = Time.now.strftime('%Y-%m-%d-%H-%M-%S')
      # try and make the callgraph filename meaningful to the test-run
      filename_base = "rubyprof_#{self.class.name}_#{timestamp}"

      # Print a call tree profile to text for kcachegrind to use
      printer = RubyProf::CallTreePrinter.new(result)
      printer.print(File.open("#{path_base}/#{filename_base}_calltree.txt", 'w'), 0)
      p "All Done. Good luck profiling!"
      end
    @@register_done = true
  end
  # will setup the reguster-rubyprof function if we have set the
  # RUN_RUBYPROF constant to true
  setup :register_rubyprof if RUN_RUBY_PROF

Tuesday, 28 July 2009

rails gotchas: undefined method `expects'

If you've just installed rails edge and run the tests, they may fail with: NoMethodError: undefined method `expects' for ... etc etc over and over again (along with a few other errors).

Install the mocha gem via rubygems to get rid of a lot of these errors.

It was the line NoMethodError: undefined method `stub' for ... that clued me in.

It seems that rails requires a short list of gems/packages (beyond rails itself) simply to run its own tests... yet there is no rake gem:install task available to help you figure out which ones... and they aren't in the "contributing to rails" guide. I'll be submitting a patch shortly...

Following on from this I got:

MissingSourceFile: no such file to load -- openssl

and farther down the list:

LoadError: no such file to load -- net/https

Unfortunately, these errors occur when Ruby hasn't been installed with openssl-support. If you're running ubuntu, you can just run apt-get install libopenssl-ruby.

Thursday, 21 May 2009

Testing gotchas: nested logins don't work

*sigh* after a couple of hours futile hacking about I finally realised that my controller tests were failing because login_as cannot be nested. ie. if you have a context with a login_as @user1 in the setup... then put a context inside it where you login_as @user2... it won't actually do the second login.

So just move that nested context outside. Simples neh?

Thursday, 30 April 2009

Should have form fields (macro)

Quickie snippet form-macro to make sure that the page you're on has the given set of form fields. You can optionally pass in a model name for "form_for" style forms (because these are pretty common), but it also lets you just pass in straight field names (for when you check your submits and hand-crafted tags).

Stick this at the bottom of your test_helper.rb and use as below:
should_have_form_fields [:name, :email, :phone, :gender], 'user'

Note: should work for text inputs as well as radio buttons and checkboxes (which is why we check for name not id - also means you don't match the label by accident). This assumes you've used Rails' helper_methods to generate your form fields - or that you have included a name on every field.

class ActionController::TestCase
  # A macro to check that we should now have form fields for the given
  # columns.
  def self.should_have_form_fields(fields, model_name = nil)
    return if fields.blank? # short circuit
    fields = [fields] unless fields.respond_to?(:[]) # arrayify
    fields.each do |f|
      should "have field #{f} on form" do
        assert_select 'form' do
          if model_name.blank?
            assert_select "[name=#{f}]"
          else
            assert_select "[name='#{model_name}[#{f}]']"
          end
        end
      end
    end
  end
end

Monday, 27 April 2009

Testing ActiveResource

I've been banging my head against the wall that is ActiveResource for a while now. One big problem is actually getting the testing correct.

Take 1: HTTPMock

The supposedly sanctioned expectation is that you test it against the provided HTTPmock... unfortunately, this is great for testing that ActiveResource makes the correct remote calls eg, when you update a user that ARes POSTs to /users/123.xml... and that it reacts to a (pre-stubbed) 404 by raising a ResourceNotFound; but it doesn't allow you to actually test your model - eg to assert that when you update your user's login field and reload your model, that your user can now login with the new details...

AFAICS you can't test anything that is actually meaningful or useful to your business logic - which kinda defeats the point IMO.

Even with a lot of hacking, I couldn't get the kind of dynamic mock backend that I needed for any meaningful test-coverage.

Take 2: Mocking with Stump

Next up was to try to mock/stub out the backend functions appropriately. I'd heard some good stuff about the stump plugin and dutifully installed it to have a go.

Stump let me do much more dynamic testing. I could stub out the "create" and "find" functions and make it respond with a mock remote object that I could then further stub out the "save" and "update" functions... and after a while it seemed like I was stubbing out functions to return stubbed out stubs and it all got a little bit circular... and in many cases: really complex.

I also found that I got the code to work in the browser I'd often still have to spend ages getting the tests to work with the right set of mocks and stubs to patch all the ways that rails could leak out and try to call the "real" API. :P

I also noticed a few times where I'd somehow accidentally plastered over a bug with a stub by assuming the return-value. I only noticed this because the tests would run fine - yet trying the same operation in the browser revealed the bug.

Now how can I test if I can't rely on the test to actually test? My safety net suddenly has big gaping holes in it that I'll only find if I fall through them while testing by hand! This is such a step backwards that it's horrifying!

Then I hit a wall.

We have a user and during creation (and update) we need to check that the login is unique - which requires a find on the remote API. So in the unit tests we mock out the User object's "find" method to return an empty set... (ie no users were found that match this login field). But then (immediately after creation), we need to be able to find the user - we we re-stub out User.find to return the given user - otherwise the test fails.
... but when you're doing a functional test, all of the above operations are inside the atomic post :create call. You can't stub out one half, then stub out the other half of the operation partway through... because you can't get to the user object partway through the process... so creates were suddenly failing badly with seemingly no solution.

The code didn't do what I needed, and on top of this was tangled and messy and really too ugly. When it comes to rails, ugly code screams out for another solution...

Take 3: A real API

So, I turned to (what I see as) the lesser of two evils. I have created a test-API project that will simulate the real remote API with the models that are used by the (local) project. the test environment calls this API instead of the "real" one by using a defined constant for the site.

In setup I send the ActiveResource model a delete_all which will clear the remote db[1]. This simulates what Rails testing does anyway (ie clearing out the db before each test).

So now the project tests against a real, live-but-fake API that I can run in another window on my local machine. The tests run - they look pretty much the same as my ActiveRecord tests - which means I can check everything I need to check. It also means I can be assured that the code hasn't plastered over a bug with mocks and stubs.

Conclusion:

Pros:

  • Real end-end tests
  • Dynamic remote object instead of static mocks (means you can test that values are changed and that reloading returns what you expect)
  • Fewer assumptions means the test-code is less likely to be buggy - ie you're more likely to be actually testing what you think you're testing.
  • Easier to test as you don't have to mock out things that will return things that you've mocked...
  • Above leads to a more natural Railsy test syntax.

Cons:

  • Keeping two projects in synch
  • extra development time of the fake API
  • Will need to make sure the API a) exists and b) is running before you can run tests
  • Have to verify that the mock API does what the real API does.
  • Tests run slower as there's the (local) network turnaround time to consider.

In my mind, the pros outweigh the cons. YMMV

Notes:
[1] We're currently not using fixtures anyway, but it we were - I would then send the remote API a "please load up your fixtures" command.

Wednesday, 22 April 2009

should_set_the_flash better

should_set_the_flash_to doesn't do quite what I'd like. I want to: a) specify which level of flash should be set (ie make sure it's a notice and not an error) and b) not care exactly what notice has been set.

IMO tying your tests down too hard to ephemeral strings is annoying... what is somebody changed the wording from "user created" to "thanks for signing up"? You have to change your test cases for that? :P So, being able to just enter "anything" should be possible.

Note - it piggybacks on the existing flash shoulda macro, and thus allows you to use it as before (pass 'nil' for level if you want - but it's better just to use the original if you need it.

So, here's my updated code. Dump it into the bottom of your test_helper file.

class ActionController::TestCase
  # Make sure that a message has been set at the given flash level
  # you can test that a notice has been posted, but no error thus:
  # should_set_flash :notice
  # should_not_set_flash :error
  def self.should_set_the_flash(level = nil, val = :any)
    val = /.*/i if val == :any
    return should_set_the_flash_to val unless level
    if val.blank?
      should "have nothing in the #{level} flash" do
        assert flash[level].blank?, "but had: #{flash[level].inspect}"
      end
    else
      should "have something in the #{level} flash" do
        assert !flash[level].blank?, "No value set of given flash level: #{level}"
      end
      should "have #{val.inspect} in the #{level} flash" do
        assert_contains flash[level], val, ", Flash: #{flash[level].inspect}"
      end
    end
  end
  # convenience method for making sure nothing has been set for the given
  # flash level
  def self.should_not_set_the_flash(level = nil)
    should_set_the_flash level, nil
  end
end

[Edit: 22-Apr-2009] : ADDED a convenience method for "not_set" and updated "set" to take advantage of this. Also updated comments to include an example of use

Thursday, 16 April 2009

rails gotchas: nested transactions

So I've been adding some new plugins for testing that I've never tried before, in this case: shoulda and stump, and found that stump just isn't playing nice with SQLite :P

The problem only occurred when I tried to declare a proxy! function on an existing model. ok, I didn't get around to trying everything, so maybe it occurs on other things too, but it seemed to be happy with stub and mock.

The code would simply fail with the following error:

SQLite3::SQLException: cannot start a transaction within a transaction
<insert useless backtrace here>

It's been driving me crazy for the last few days trying all sorts of ways to get it to work. I almost abandoned transactional fixtures entirely before I finally found this snippet at the very *bottom* of the ActiveRecord::Transactions page in APIdock

"Most databases don’t support true nested transactions. At the time of writing, the only database that we’re aware of that supports true nested transactions, is MS-SQL."

So the problem is not transactions... but SQLite's lack of support of proper nested transactions.

The solution: Install MySQL.

It's annoying to have to go back to creating a MySQL db/user and granting rights before I can run my tests - but if it makes the test code work, then it's worth that extra step.

SQLite is nice, but it's ability to blow away the db by just doing a quick rm isn't worth not being able to properly test my code. Besides - the real db is on MySQL anyway, so I figure I might as well.

Thursday, 9 April 2009

Rails gotchas: shoulda not_allow_values_for

If you're using should_not_allow_values_for and getting a failing test something in the lines of:

Failure:
test: User should not allow email to be set to "b lah". (UserTest)
    [/usr/lib/ruby/gems/1.8/gems/thoughtbot-shoulda-2.10.1/lib/shoulda/assertions.rb:56:in `assert_rejects'
     /usr/lib/ruby/gems/1.8/gems/thoughtbot-shoulda-2.10.1/lib/shoulda/active_record/macros.rb:174:in `__bind_1239266378_671612'
     /usr/lib/ruby/gems/1.8/gems/thoughtbot-shoulda-2.10.1/lib/shoulda/context.rb:253:in `call'
     /usr/lib/ruby/gems/1.8/gems/thoughtbot-shoulda-2.10.1/lib/shoulda/context.rb:253:in `test: User should not allow email to be set to "b lah". ']:
Expected errors to include "is invalid" when email is set to "b lah", got errors: email is too short (minimum is 6 characters) ("b lah")email should look like an email address. ("b lah")

You can see in the above test that the email does have an error on it - but the error message is not the default. shoulda specifically checks for the error message - and if you don't have the default, then you need to pass it in thus:

should_not_allow_values_for :email, "b lah", :message => Authentication.bad_email_message

You shoulda test your plugin!

So, lets assume you have a plugin "acts_as_teapot" that you want to include in some of your model classes. You've written a number of useful methods for teapot-like models to use, and now want to check that the models that are implementing acts_as_teapot actually are able to make use of the full spectrum of teapotly functionality.

You could write a set of asserts that you can tell your plugin's users to "please include these asserts/tests in your model classes"... but it's not DRY, and the users might miss one, and they'll get out of date real quick... what you want is kinda one big assert they can just put in once and that calls some library on the plugin itself (so it keeps up to date with the latest plugin code).

Luckily, shoulda is here to save the day! You can create a big context full of all the right tests and save it in the plugin file itself. This will be drawn in by the model at the time it's included. Then the user just has to call a single "shoulda" and it's all done for you.

Now, I was against shoulda for a long time - mainly wondering why anybody would use a half-arsed version of rSpec if they didn't actually want rSpec... but for me, plugin-testing is the killer-app that forced me to re-evaluate shoulda, and so far it actually looks ok. :)

So, to the code...

Plugin code

module Acts
  module Teapot
    def describe_me
      "short and stout"
    end
    def tip_me_over
      "pour me out"
    end
  end
end
class Test::Unit::TestCase
  def self.should_act_as_a_teapot
    klass = model_class

    context "A #{klass.name}" do
      setup { @new_klass = klass.new }

      should "respond to teapotly functions" do
        [:tip_me_over, :describe_me].each do |f|
          assert @new_klass.respond_to?(f), "#{klass.name} should respond to the function: #{f}"
        end
      end
      should "be short and stout" do
        assert_equal "short and stout", @new_klass.describe_me, "#{klass.name} is a funny-looking teapot."
      end
      should "pour me out" do
        assert_equal "pour me out", @new_klass.tip_me_over, "#{klass.name} doesn't make very good tea!."
      end
    end
  end
end

and testing the model:

class MyTeapot < ActiveRecord::Base
  acts_as_a_teapot
  #...
end

class MyTeapotTest < ActiveSupport::TestCase
  fixtures :my_teapots

  # plugin contexts
  should_act_as_a_teapot

end

There's a real-world example in the paperclip shoulda test

rails gotchas: assert_raises a syntax error

I found that assert_raise was causing a syntax error of the form:
syntax error, unexpected '{', expecting kEND (SyntaxError)
For a fairly simple test:

should "remove it from the database" do
  assert_raise ActiveRecord::RecordNotFound { User.find(@uid)}
end

Looks like it's getting confused about nesting. the solution? parenthesise your Error thus:

should "remove it from the database" do
  assert_raise(ActiveRecord::RecordNotFound) { User.find(@uid)}
end

Monday, 6 April 2009

rails gotchas: HttpMock not enough variables

A quickie for my own remembrance. I'm currently setting up HttpMock to test ActiveResource. I'd set up a few extra "routes" for it to mock out and kept getting the error below:

NoMethodError: undefined method `size' for :not_found: Symbol

For the code line:

mock.get    "/users/#{uid}.xml", {}, :not_found

The fix was pretty simple - I'd just accidentally missed out the "nil" for the body - ie it was actually an ArgumentError - but not getting picked up. ie, the code should have been:

mock.get    "/users/#{uid}.xml", {}, nil, :not_found

Thursday, 25 September 2008

Rspec mocks too much

I've recently had to learn the other side of testing - ie rspec, due to my new contract having a test suite written in "the other testing suite" * ;)

Seems fairly straightforward so far, but there's one big worry I have: Rspec has a huge reliance upon mocks.

Now I completely understand (and endorse) the benefits of using mocks. It means you can independantly test a model/controller/whatever without it mattering if something happens to have broken in some other model/module/whatever. This is a Good Thing and allows for more accurate testing due to all the benefits of loose coupling. However, I get a bit worried if everything is mocked out and there are no integration-style tests at all.

For example, if I'm testing a view, and using rspec best practice I've mocked out all model objects and don't bother with the real thing. What happens if a model changes (say a column is removed that shows up on an index list)? The view specs all rely on a mock that explicitly states what is returned, so the view specs all still pass. But the index page will be broken for a user that actually uses the site, because the real object doesn't bahave that way anymore. The mocks and the model are out of synch. :P

In an infrequently-used area of the site, this sort of error may go undetected and slip through to release.

This worries me. I know I like to rely on my test suite to catch things like this wherever possible. Sure I generally do a manual click-through, but I know that this is unreliable especially WRT edge-cases.

So, am I needlessly worrying? Have I simply missed something about how rspec is supposed to be used? What's best practice in this situation?

*Which admittedly feels a lot like having to switch from vi to emacs... Sure I know that "the other one" works for lots of people, and I meant to get around to learning it someday...

Tuesday, 17 June 2008

assert email sent_to recipients

We have notifications sent out to users when their orders are finished processing. Recently we added "user groups" and so orders (which used to belong to a single user) now belong to groups of users. Which brought about the possibilty that a user could be removed from the group (and therefore could no longer see the order).

It's possible that this could happen between the time they submit their order and the time that the order has finished processing. We determined that, if the user no longer belongs to the group, they shouldn't get the notification - it should go to the next person in the group.

So our original notification code had something along the lines of:

  # Prepares an email that will notify the user that his calculation is complete
  def report_complete_notification(report)
    setup_common_values(report.user)
    @subject    += 'Your report is complete'
    body[:report] = report
  end

  # Details common to all emails from us
  def setup_common_values(user)
    recipients  user.email
    from        DEFAULT_FROM_EMAIL
    subject     "Our Funky Service - "
    sent_on     Time.now.utc
    body        :user => user
  end

So first we needed to update the notifier to call an appropriate 'recipient' if needed:

    setup_common_values(report.recipient)

Using the following recipient code in the model object:

  def recipient
    # always belongs to the original owner if no group is specified.
    return self.user unless self.group
    # use same recipient if creator still belongs to the request's group
    return self.user if self.group.users.include?(self.user)
    # otherwise grab the first person out of the group
    return self.group.users.first unless self.group.users.blank?
    # no users left - who do we notify?
    raise "HELP! The report: #{self.id} belongs to group: #{self.group.id}, but the group has no users so I don't know who should receive my notifications."
  end

As you can see, we aren't yet sure where to escalate notificaton if the group is completely empty. We have several options, but we're still waiting on the customer to get back to us and tell us what they want. The most likely option is to send an alternative notification to our admins.

How to test?

So now we need to test whether the appropriate person gets the notification. To begin with, testing whether an email went to a recipient, or set of them, isn't a pretty assertion, so I've abstracted it out into its own assertion (below). This also allows me to extend it if we need to check CC: and BCC: lists later. For now we don't use them, so "to" is good enough.

  # assert that the given recipients appeared in the recipient list of
  # the given email. If "want_match" is false - assert that *none* of the
  # given recipients were in the recipient list
  def assert_recipients(eml, recipients, want_match = true)
    recipients = [recips] unless recipients.respond_to?('[]') # arrayify if single item
    recipients.each do |recip|
      match = eml.to.any? {|r| r =~ /#{recip}/}
      match = !match unless want_match
      msg = want_match ? "email should have been sent to recipient #{recip}, but instead went to: #{eml.to}" : "email should not have been sent to recipient #{recip}"
      assert match, msg 
    end
  end
  # convenience function for asserting an email had recipients
  def assert_sent_to(eml, recips)
    assert_recipients eml, recips
  end
  # convenience function for asserting an email did not have recipients
  def assert_not_sent_to(eml, recips)
    assert_recipients eml, recips, false
  end

Now we can do various tests such as:

  # note - assume mailer.deliveries is instantiated as @emails in setup.
  def test_should_send_completion_notification
    order = orders(:completed)
    
    UserNotifier.deliver_completion_notification(order)
   
    assert_equal 1, @emails.size, "should have received one email to confirm completion"
    assert_sent_to @emails[0], order.user.email
  end
  def test_should_send_completion_notification_to_new_recipient_if_user_moved
    order = orders(:completed_for_moved_user)
    # sanity check
    assert_not_equal order.user, order.recipient, "user has moved group, but they are still showing up as the recipient"
    
    UserNotifier.deliver_completion_notification(order)
   
    assert_equal 1, @emails.size, "should have received one email to confirm completion"
    assert_sent_to @emails[0], order.recipient.email
    assert_not_sent_to @emails[0], order.user.email
  end

Wednesday, 21 May 2008

Rails gotchas: more TzTime

While I'm neck-deep in timezone land, I'll also address one little problem wih the tztime plugin. While running the functional tests I began getting errors like the following:

NoMethodError: undefined method `tzinfo' for #<TZInfo::LinkedTimezone: UTC>

I could fix it by forcing a timezone into the TzInfo object at the beginning of each failing test - but that seemed a bit of a hack. Looks like the problem has been submitted as a bug in rails ticket 11150. The suggested codefix seems to work just fine, but requires more hacking on your vendor/plugins/tztime directory. Still, it's a one-line fix, so not much to do.

The failing tests were replaced with a new error:

<Tue Apr 01 00:00:00 +0000 2008> expected but was
<2008-04-01 00:00:00 UTC>.

The failing line is in the functional test thus:

assert_equal Time.now.at_beginning_of_month, assigns(:month)

This is an instance of a problem I've seen before... ie that you don't seem to be able to compare Times with DateTimes, Dates or (now) TzTimes. The quick solution is to forcibly cast all your time-like objects into a common time-like object before testing for equality. My suggestion is to_datetime, which works for all the instances I've needed, thus the previous test case would become:

assert_equal Time.now.at_beginning_of_month.to_datetime, assigns(:month).to_datetime