David Chelimsky
2009-Sep-25 09:56 UTC
[rspec-users] How to spec transaction in Controller ?
On Fri, Sep 25, 2009 at 3:06 AM, Andrea Jahn <anja-email1 at web.de> wrote:> Hi, > > in my Controller I have some tansactions. I''m trying to write specs, which test the two program pathes (an exception is thrown or no exception is thrown). But the test seems always to go into the path, where the exception is catched. > > Here''s an example for the delete action > (destroy method is overwritten, so :dependent => :destroy is not possible here)Off topic, but ActiveRecord offers a number of ways to interact with model and transaction life cycles. The fact that you are overriding destroy rather than hooking into the destroy cycle means you''re bypassing a lot of functionality that you now have to implement yourself (hence the problem you are describing in this post). If there is any way for you to use those hooks instead, I''d strongly recommend it.> > The test case? "should flash the notice" fails, because not the program path of success is executed, but the rescue path is executed. > > Perhaps my exception handling is generally wrong ? > > Thanks > Andrea > > plannings_controller.rb > ----------------------- > > ????? def destroy > ??????? @planning = Physical::Planning::Planning.find(params[:id]) > > ??????? respond_to do |format| > ????????? begin > ??????????? ActiveRecord::Base.transaction do > ????????????? @planning.destroy > ????????????? @planning.planned_amounts.each { |planned_amount| planned_amount.destroy } > ??????????? endEven if you''ve overridden destroy, why not just include this code in the custom destroy method? Then the controller could just call @planning.destroy and that method can worry about transactions.> ??????????? # success > ??????????? flash[:notice] = "The #{Physical::Planning::Planning::DISPLAY_NAME.downcase} ''" + @planning.title + "'' was successfully deleted." > ??????????? format.html { redirec t_to(planning_plannings_url) } > ??????????? format.xml? { head :ok } > ????????? rescue => e > ??????????? # DB raised errors > ??????????? flash[:error] = "The #{Physical::Planning::Planning::DISPLAY_NAME.downcase} ''" + (@planning.nil? ? "" : @planning.title) + "'' could not be deleted. Error:" + e > ??????????? format.html { redirect_to(planning_plannings_url) } > ??????????? format.xml? { render :xml => @planning.errors, :status => :unprocessable_entity } > ????????? end > ??????? end > ????? end > > > > plannings_co ntroller_spec.rb > ---------------------------- > > describe Physical::Planning::PlanningsController, "DELETE" do > ? include PlanningMacros > > ? should_require_login :put, :delete > ? should_require_authorization :put, :delete > > ? context "when the user is logged in" do > ??? before(:each) do > ????? do_login > ????? do_authorization > > ????? @planning = mock_model(Physical::Planning::Planning, valid_planning_attributes) > ????? @planned_amount = mock_model(Physical::Planning::PlannedAmount, valid_planned_amount_attributes) > > ????? @planning.stub!(:destroy) > ????? @planning.stub!(:planned_amounts).and_return([@planned_amount]) > > ????? Physical::Planning::Planning.stub!(:find).and_return(@planning) > > ????? @plan ning.errors.stub!(:full_messages).and_return([]) > ??? end > > ??? # delete successfully > ??? # ------------------- > > ??? context "and the Planning deletes successfully" do > > ????? .... > > ????? it "should destroy the Physical::Planning::Planning in a transaction" do > ??????? Physical::Planning::Planning.should_receive(:transaction).any_number_of_times.and_yield( > ????????? @planning.should_receive(:destroy) > ??????? )This ^^ isn''t doing anything. any_number_of_times includes 0, which is the number of times Planning (the class object) receives transaction (the controller calls ActiveRecord::Base.transaction). I''d recommend leaving out any_number_of_times. By default, should_receive expects exactly one call, which is the right number in this case. And it should be on ActiveRecord::Base, given the current state of the implementation. This, however, is a great example of how writing examples first can have a positive impact on design. If I were writing this, I never would have specified transactions in a controller action. This example would simply be: it "should destroy the Physical::Planning::Planning in a transaction" do @planning.should_receive(:destroy) do_put_delete end Then the implementation would only need call @planning.destroy to pass the example. The transactional nature of the destroy method would be specified in the model spec, and now any other code that gets written later that needs to destroy a Planning instance can just send it destroy() and you won''t have to duplicate this transaction code in each of those cases. HTH, David> ??????? do_put_delete > ????? end > > ????? it "should destroy the PlannedAmounts of the Physical::Planning::Planning in a transaction" do > ??????? Physical::Planning::Planning.should_receive (:transaction).any_number_of_times.and_yield( > ????????? @planned_amount.should_receive(:destroy) > ??????? ) > ??????? do_put_delete > ????? end > > ????? it "should flash the notice" do > ??????? do_put_delete > ??????? flash[:notice].should match(/was successfully deleted/) > ????? end > > ????? ... > > ??? end > > ??? # delete fails > ??? # ------------ > ??? context "and the Planning fails to delete" do > > ????? it "should flash error message because of database exception" do > ??????? Physical::Planning::Planning.should_receive(:transaction).any_ number_of_times.and_yield( > ????????? @planning.should_receive(:destroy).and_raise(ActiveRecord::RecordInvalid.new(@planning)) > ??????? ) > ??????? do_put_delete > ??????? flash[:error].should match(/could not be deleted/) > ????? end > ??? end > ? end > end