Forum: Ruby on Rails Quick functional test question

Announcement (2017-05-07): www.ruby-forum.com is now read-only since I unfortunately do not have the time to support and maintain the forum any more. Please see rubyonrails.org/community and ruby-lang.org/en/community for other Rails- und Ruby-related community platforms.
Chris T (Guest)
on 2006-04-25 20:48
(Received via mailing list)
Preamble: I've been putting off getting into testing while I got my head
around everything else, because I am a BAD PERSON, and thought they
wouldn't be fun. After the past couple of hours I know I was wrong to
put it off and right that they wouldn't be fun (Didn't help that took me
a while to realise the Agile Book is out of date on unit testing).

Anyway I have a small method in a controller that parses a release date
supplied by Amazon into a date field. Took a bit of thinking about the
best way of doing it but seems to work. however, thought this would be a
good time to start testing so i can throw all sorts of different inputs
at it, but the test is not recognizing the method. In the controller
I've got::

class ContributeController < ApplicationController
...
private
  def parse_amazon_date(date)
  ...
  end
end

and in my functional test I've got:

require File.dirname(__FILE__) + '/../test_helper'
require 'contribute_controller'
....
  def test_amazon_date_parse
    assert_equal Time.gm(2000, "oct", 25), parse_amazon_date("25
October, 2000")
    #other tests
  end
end

All I keep getting when i try to run the test is:

NoMethodError: undefined method `parse_amazon_date' for
#<ContributeControllerTest:0x4096aea4>

/usr/local/lib/ruby/gems/1.8/gems/actionpack-1.12.1/lib/action_controller/test_process.rb:432:in
`method_missing'
    test/functional/contribute_controller_test.rb:25:in
`test_amazon_date_parse'

Possible I'm being really dense due to a particularly shitty cold, but
if anyone could point me in he right direction, that'd be great.
Brian H. (Guest)
on 2006-04-25 21:00
(Received via mailing list)
On Apr 25, 2006, at 12:43 PM, Chris T wrote:
> require File.dirname(__FILE__) + '/../test_helper'
> require 'contribute_controller'
> ....
>  def test_amazon_date_parse
>    assert_equal Time.gm(2000, "oct", 25), parse_amazon_date("25
> October, 2000")
>    #other tests
>  end
> end

I'm pretty sure that your problem stems from the fact that
parse_amazon_date is a private method of your ContributeController
class. If you take a look at the top of your functional test file,
you'll see that the controller class in required into the functional
test script. But all that does is make the class and it's public
methods available to your test class. That won't give you outside
access to a private class method. Tagging a method as private means
only methods within the parent class can directly call it.

> All I keep getting when i try to run the test is:
>
> NoMethodError: undefined method `parse_amazon_date' for
> #<ContributeControllerTest:0x4096aea4>
>    /usr/local/lib/ruby/gems/1.8/gems/actionpack-1.12.1/lib/
> action_controller/test_process.rb:432:in `method_missing'
>    test/functional/contribute_controller_test.rb:25:in
> `test_amazon_date_parse'

Notice how the error message is telling you that it can't find the
method you want, inside the test class... This is because it can't
see private methods in your controller class. If you want to be able
to test that method you'll need to decide on a way for it to be
accessible through a public interface.

-Brian
Chris T (Guest)
on 2006-04-25 21:07
(Received via mailing list)
Just removed the private statement and it's made no difference. Damn,
thought that might work
Brian H. (Guest)
on 2006-04-25 22:06
(Received via mailing list)
On Apr 25, 2006, at 01:06 PM, Chris T wrote:
> Just removed the private statement and it's made no difference.
> Damn, thought that might work

Hmm... OK. I just realized something. You're not actually calling
into the controller correctly. Based on your code, I'd say you are
trying to build your functional test as though it were a unit test.

>>
>> require File.dirname(__FILE__) + '/../test_helper'
>> require 'contribute_controller'
>> ....
>>  def test_amazon_date_parse
>>    assert_equal Time.gm(2000, "oct", 25), parse_amazon_date("25
>> October, 2000")

This statement is more what you would use in a unit test. But even in
that case, you'd still get an error, since what you're specifying is
a method of the current class, not of another class. In reality, I
think that's no so bad, since what you're trying to do doesn't really
belong in your controller. Controller methods, aka. actions, are
there to catch incoming web requests. So when you are doing
functional testing you actually start the test off by simulating a
web request, like this:

   def test_my_silly_controller
     get :index
     assert_template 'silly/index'
   end

This somewhat useless test case calls the index action/method of
SillyController, like a browser would, and then verifies that the
controller renders the proper view. It's somewhat useless, since 95%+
of the time the proper template is always rendered by your action.

This isn't at all what you are trying to test here, so what I
recommend you do is create a custom AmazonDate class. Basically, you
create an amazon_date.rb file in your app's lib directory and put a
"class AmazonDate ... end" in it.

Then you place your class method inside there, like this:

   class AmazonDate
     def self.parse (adate)
     ...
     end
   end

 From this, you can then make a new unit test called
amazon_date_test, where you can then put your normal unit tests to
verify that your date parser is doing the right things.

-Brian
Chris T (Guest)
on 2006-04-25 22:46
(Received via mailing list)
I did wonder whether putting it in the controller was entirely the right
place, but thought it was better there than in the model (since it's not
really to do with the business logic), and is only referenced by that
controller. Haven't explored custom classes and the lib folder yet, but
guess it may be time to. Thanks for your help
Chris T (Guest)
on 2006-04-26 02:33
(Received via mailing list)
Brain
Had another think about this, and I'm not doubting you're right just
trying to understand things a bit better. Are you saying you shouldn't
put methods in controllers that aren't there to catch actions directly -
i.e. those methods that are just servicing the 'action' methods should
go elsewhere (this doesn't make 100% sense to me, not least because
surely the methods in the application controller are never called
directly)? Or is it that they can't be tested atomically -- only as part
of the visible action?

Also does seem a bit sledgehammer to crack a nut, when it's only a
5-line method.  I suppose the alternative is to test the method
indirectly, through passing various fixtures through the visible case,
though this strikes me as a slightly less pure way of doing it. Still
not quite understanding it...
Brian H. (Guest)
on 2006-04-26 04:45
(Received via mailing list)
On Apr 25, 2006, at 06:25 PM, Chris T wrote:
> Had another think about this, and I'm not doubting you're right
> just trying to understand things a bit better. Are you saying you
> shouldn't put methods in controllers that aren't there to catch
> actions directly - i.e. those methods that are just servicing the
> 'action' methods should go elsewhere (this doesn't make 100% sense
> to me, not least because surely the methods in the application
> controller are never called directly)?

Not at all. There's all kinds of reasons for non-action methods in
controllers, but none of those reasons should include performing
business logic on a data object. That business logic is part of the
object's behavior, which you should definitely be accessing from the
controller, but that's not where the business logic should go.

> Or is it that they can't be tested atomically -- only as part of
> the visible action?

This is probably a good indication that you are working with an
object's behavior and that's something that should be part of the
model-layer, in the Rails MVC architecture. Just remember, not all
"models" have to be based on ActiveRecord.

> Also does seem a bit sledgehammer to crack a nut, when it's only a
> 5-line method.  I suppose the alternative is to test the method
> indirectly, through passing various fixtures through the visible
> case, though this strikes me as a slightly less pure way of doing
> it. Still not quite understanding it...

I think what you need to do is stop thinking of it as "only a 5-line
method." Referring to it that way takes you down the road of
procedural programming, which you can do, but that's really not a
place you want to go.

You've got a business object, AmazonDate, that you need to operate
on. Actually, re-reading the description in your OP, it actually
looks like what you need is a way to build a standard Ruby Date from
a custom supplied string representation. You should probably do this,
and this might be "more correct", as an extension of the standard
Ruby Date class. Something along these lines:

class Date
   def self.from_amazon_date(amazon_date)
     ... # your parsing statements, making sure you return a Date object
   end
end

Again, I would place this into a script file, called something like
amazon_date.rb, in the top-level of your app's lib folder. What we're
doing here is specifying a custom behavior, within a standard Ruby
class. These kinds of dynamic class extensions happen all throughout
Rails and I think is the right fit for what you're looking to do...

-Brian
Chris T (Guest)
on 2006-04-26 11:37
(Received via mailing list)
OK. I think I understand where you're coming from. I think it's one of
those things that, coming from a non-OO background takes a bit of
getting used to. It's particularly difficult when you've done the
classic newbie route like I have -- coming from PHP, trying the rails
tutorials, then reading the Agile book, only after then starting to
learn Ruby (and no OO experience at all).

In cases like this, it's doubly difficult, because there are lots of
ways the parsing could have been incorporated into the program (even
though there may only be one correct OO way): I could quite easily have
incorporated the parsing of the Amazon date into the action (as that's
the only place it's used), or could have incorporated it into a view,
possibly via a helper (because the resulting Date object is used only to
populate a date select field), or could incorporate it into the Book
model as an instance method, as getting a correct Book object is the
final goal.

I guess what I'm saying is when you approach this from a non-OO
viewpoint, there's a lot of ways to skin this particular cat, all of
which are superficially easier than this route. However, for what it's
worth, when I've been forced to go down a true OO route, I've tended to
be grateful in the long run.

Thanks for your help.
This topic is locked and can not be replied to.