Jonathan Stott
2008-May-03 12:08 UTC
[mocha-developer] Need some help in reducing my repetions in tests with Mocha
Hi All I''m writing a gem to interact with the todoist JSON API and I''m using mocha to mock the server responses so I don''t have to have a net connection to test and so I don''t need to actually use my own API key anywhere. Mostly I am interested in verifying that a correctly formatted URL has been sent, with the appropriate parameters, e.g API key have been sent. So I end up with expectations like so: it "includes a token in the parameters" do http = mock() http.stubs(:use_ssl=) http.expects(:get).with { |url| url.split("?",2).last =~ /token=#{@token}/ }.returns(good_response) Net::HTTP.stubs(:new).returns(http) @base.query("viewall") end But the problem is, all my expectations basically feature the same lines of code (from http = mock() to Net::HTTP stubs) with only the "with" block changing. But because it is the with block changing, I''m not sure how to create a method which will insert my changing block into the middle of the expectation call. Well, I have a prototype method: def web_stub(good=true,&block) http = mock() http.stubs(:use_ssl=) get = http.expects(:get) if block_given? get = yield get end get.returns(good ? good_response : bad_response) Net::HTTP.stubs(:new).returns(http) end But this requires me to use the very ugly syntax of web_stub do |get| get.with do |url| url.split("?",2).first =~ /query$/ end end And I''d like to avoid the need for nested blocks if possible. Any pointers on how to implement the method, or a better way of doing it in general would be appreciated! Regards, Jon
Jay Fields
2008-May-03 12:49 UTC
[mocha-developer] Need some help in reducing my repetions in tests with Mocha
Hi Jon, It''s hard to give good advice because we are missing too many pieces of information. For example, your test references both @base and "good_response", neither of which are shown elsewhere. Also, without your domain code it''s hard to say what the best way to test it is. But, not having enough information has never stopped me from giving my opinion before... I looks to me like you should create smaller methods, if not a few more classes depending on how much is going on. For example, you want to test that the token is in the parameters, so why not create a method that returns the url. If you have a method that returns the url, then you can test that method in isolation. On my last project I used Net::HTTP, and I understand it can be a bit of a tangled web, but I created a Gateway class that hid all the complexity of Net::HTTP from my domain. The Gateway class also had many small methods that I could test in isolation or as part of larger tests. The small methods are helpful in the larger tests because you can stub the methods you don''t care about. Cheers, Jay On May 3, 2008, at 1:08 PM, Jonathan Stott wrote:> Hi All > > I''m writing a gem to interact with the todoist JSON API and I''m > using mocha to mock the server responses so I don''t have to have a > net connection to test and so I don''t need to actually use my own > API key anywhere. > > Mostly I am interested in verifying that a correctly formatted URL > has been sent, with the appropriate parameters, e.g API key have > been sent. So I end up with expectations like so: > > it "includes a token in the parameters" do > http = mock() > http.stubs(:use_ssl=) > http.expects(:get).with { |url| > url.split("?",2).last =~ /token=#{@token}/ > }.returns(good_response) > Net::HTTP.stubs(:new).returns(http) > @base.query("viewall") > end > > But the problem is, all my expectations basically feature the same > lines of code (from http = mock() to Net::HTTP stubs) with only the > "with" block changing. But because it is the with block changing, > I''m not sure how to create a method which will insert my changing > block into the middle of the expectation call. > > Well, I have a prototype method: > > def web_stub(good=true,&block) > http = mock() > http.stubs(:use_ssl=) > get = http.expects(:get) > if block_given? > get = yield get > end > get.returns(good ? good_response : bad_response) > Net::HTTP.stubs(:new).returns(http) > end > > But this requires me to use the very ugly syntax of > > web_stub do |get| > get.with do |url| > url.split("?",2).first =~ /query$/ > end > end > > And I''d like to avoid the need for nested blocks if possible. > > Any pointers on how to implement the method, or a better way of > doing it in general would be appreciated! > > Regards, > Jon > _______________________________________________ > mocha-developer mailing list > mocha-developer at rubyforge.org > http://rubyforge.org/mailman/listinfo/mocha-developer
John D. Hume
2008-May-03 14:31 UTC
[mocha-developer] Need some help in reducing my repetions in tests with Mocha
I''d second Jay''s suggestion to have an object whose responsibility is to compose the request. You can test that without any of the noise of the Net::HTTP API. That object could also potentially expose the bits its composed to keep you from having to do something as unreadable as url.split(''?'', 2).last =~ /token=#{@token}/ for every parameter. Maybe one object''s job is to bring together the elements of the todoist request and another''s is to format requests into URLs (or with slightly broader scope, to use them to interact with Net::HTTP), so you can spec the former with lines like url.params[''token''].should == @token and the other can be specified without the details of the todoist API getting in the way. -hume. On Sat, May 3, 2008 at 8:49 AM, Jay Fields <jay at jayfields.com> wrote:> Hi Jon, > > It''s hard to give good advice because we are missing too many pieces of > information. For example, your test references both @base and > "good_response", neither of which are shown elsewhere. Also, without your > domain code it''s hard to say what the best way to test it is. > > But, not having enough information has never stopped me from giving my > opinion before... > > I looks to me like you should create smaller methods, if not a few more > classes depending on how much is going on. For example, you want to test > that the token is in the parameters, so why not create a method that returns > the url. If you have a method that returns the url, then you can test that > method in isolation. > > On my last project I used Net::HTTP, and I understand it can be a bit of a > tangled web, but I created a Gateway class that hid all the complexity of > Net::HTTP from my domain. The Gateway class also had many small methods that > I could test in isolation or as part of larger tests. The small methods are > helpful in the larger tests because you can stub the methods you don''t care > about. > > Cheers, Jay > > > > On May 3, 2008, at 1:08 PM, Jonathan Stott wrote: > > Hi All >> >> I''m writing a gem to interact with the todoist JSON API and I''m using >> mocha to mock the server responses so I don''t have to have a net connection >> to test and so I don''t need to actually use my own API key anywhere. >> >> Mostly I am interested in verifying that a correctly formatted URL has >> been sent, with the appropriate parameters, e.g API key have been sent. So I >> end up with expectations like so: >> >> it "includes a token in the parameters" do >> http = mock() >> http.stubs(:use_ssl=) >> http.expects(:get).with { |url| >> url.split("?",2).last =~ /token=#{@token}/ >> }.returns(good_response) >> Net::HTTP.stubs(:new).returns(http) >> @base.query("viewall") >> end >> >> But the problem is, all my expectations basically feature the same lines >> of code (from http = mock() to Net::HTTP stubs) with only the "with" block >> changing. But because it is the with block changing, I''m not sure how to >> create a method which will insert my changing block into the middle of the >> expectation call. >> >> Well, I have a prototype method: >> >> def web_stub(good=true,&block) >> http = mock() >> http.stubs(:use_ssl=) >> get = http.expects(:get) >> if block_given? >> get = yield get >> end >> get.returns(good ? good_response : bad_response) >> Net::HTTP.stubs(:new).returns(http) >> end >> >> But this requires me to use the very ugly syntax of >> >> web_stub do |get| >> get.with do |url| >> url.split("?",2).first =~ /query$/ >> end >> end >> >> And I''d like to avoid the need for nested blocks if possible. >> >> Any pointers on how to implement the method, or a better way of doing it >> in general would be appreciated! >> >> Regards, >> Jon >> _______________________________________________ >> mocha-developer mailing list >> mocha-developer at rubyforge.org >> http://rubyforge.org/mailman/listinfo/mocha-developer >> > > _______________________________________________ > mocha-developer mailing list > mocha-developer at rubyforge.org > http://rubyforge.org/mailman/listinfo/mocha-developer >
Jonathan Stott
2008-May-03 20:23 UTC
[mocha-developer] Need some help in reducing my repetions in tests with Mocha
> Hi Jon, > > It''s hard to give good advice because we are missing too many pieces > of information. For example, your test references both @base and > "good_response", neither of which are shown elsewhere. Also, without > your domain code it''s hard to say what the best way to test it is.Well, a good_response is just that. It''s something the library is expecting from the server (else it will raise a BadResponse error) but otherwise it isn''t actually that important to the method I''m testing. (it''s a stub which returns the minimum possible info) Though I''ve just realised perhaps I should make it a mock and actually give it some expectations when I test the net interacting part. </digression> @base is the actual thing I''m testing which has the code which makes the API request.> > But, not having enough information has never stopped me from giving my > opinion before... > > I looks to me like you should create smaller methods, if not a few > more classes depending on how much is going on. For example, you want > to test that the token is in the parameters, so why not create a > method that returns the url. If you have a method that returns the > url, then you can test that method in isolation. >There are actually some methods which are behind the query method (though not actually so fine grained/small as to have one which makes the URL) but they are currently all protected, since they should never need to be called in isolation, except if I wanted to test them ... What is the best way of testing protected methods? Or should I just make everything public?> > Cheers, Jay >Regards, Jon
Jonathan Stott
2008-May-03 20:40 UTC
[mocha-developer] Need some help in reducing my repetions in tests with Mocha
On Sat, 3 May 2008 10:31:31 -0400 "John D. Hume" <duelin.markers at gmail.com> wrote:> That object could also potentially expose the bits its composed to keep you > from having to do something as unreadable as > > url.split(''?'', 2).last =~ /token=#{@token}/ > > for every parameter.Well, that is mostly because I don''t want to implement a full URL params parser for testing, really :) I guess it would help the clarity of the tests though. (it''s just unneeded for the library as a whole, which only ever has to construct urls)> > Maybe one object''s job is to bring together the elements of the todoist > request and another''s is to format requests into URLs (or with slightly > broader scope, to use them to interact with Net::HTTP), so you can spec the > former with lines like > > url.params[''token''].should == @token > > and the other can be specified without the details of the todoist API > getting in the way.I think I will take your and Jay''s suggestion to split my method apart a little at least, even if I don''t go for full on ''HttpQuery'' object. Thanks, Jon
John D. Hume
2008-May-04 13:30 UTC
[mocha-developer] Need some help in reducing my repetions in tests with Mocha
On Sat, May 3, 2008 at 4:40 PM, Jonathan Stott <jonathan.stott at gmail.com> wrote:> Well, that is mostly because I don''t want to implement a full URL params > parser for testing, really :)[To be clear, you wouldn''t write a parser, you''d write a "builder."] I wouldn''t characterize this as a design change just for testing: you''re separating two responsibilities: (1) knowing how to talk to todoist, and (2) knowing how to use Net::HTTP. If todoist ever changes their api, you''ll go make changes in TodoistRequestBuilder (or whatever you call that) and won''t have to touch the thing that uses Net::HTTP. If instead you find an HTTP client library with more bells and whistles (or just a prettier API), you''ll change your HTTP thing and won''t touch the TodoistRequestBuilder. Here''s a good read on the Single Responsibility Principle: http://www.objectmentor.com/resources/articles/srp.pdf