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.
on 2006-04-25 18:48
on 2006-04-25 19:00
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
on 2006-04-25 19:07
Just removed the private statement and it's made no difference. Damn, thought that might work
on 2006-04-25 20:06
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
on 2006-04-25 20:46
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
on 2006-04-26 00:33
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...
on 2006-04-26 02:45
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
on 2006-04-26 09:37
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.