require 'spec_helper' require 'split/experiment' require 'split/algorithms' require 'time' describe Split::Experiment do def new_experiment(goals=[]) Split::Experiment.new('link_color', :alternatives => ['blue', 'red', 'green'], :goals => goals) end def alternative(color) Split::Alternative.new(color, 'link_color') end let(:experiment) { new_experiment } let(:blue) { alternative("blue") } let(:green) { alternative("green") } context "with an experiment" do let(:experiment) { Split::Experiment.new('basket_text', :alternatives => ['Basket', "Cart"]) } it "should have a name" do experiment.name.should eql('basket_text') end it "should have alternatives" do experiment.alternatives.length.should be 2 end it "should have alternatives with correct names" do experiment.alternatives.collect{|a| a.name}.should == ['Basket', 'Cart'] end it "should be resettable by default" do experiment.resettable.should be_true end it "should save to redis" do experiment.save Split.redis.exists('basket_text').should be true end it "should save the start time to redis" do experiment_start_time = Time.at(1372167761) Time.stub(:now => experiment_start_time) experiment.save Split::Experiment.find('basket_text').start_time.should == experiment_start_time end it "should not save the start time to redis when start_manually is enabled" do Split.configuration.stub(:start_manually => true) experiment.save Split::Experiment.find('basket_text').start_time.should be_nil end it "should save the selected algorithm to redis" do experiment_algorithm = Split::Algorithms::Whiplash experiment.algorithm = experiment_algorithm experiment.save Split::Experiment.find('basket_text').algorithm.should == experiment_algorithm end it "should handle having a start time stored as a string" do experiment_start_time = Time.parse("Sat Mar 03 14:01:03") Time.stub(:now => experiment_start_time) experiment.save Split.redis.hset(:experiment_start_times, experiment.name, experiment_start_time) Split::Experiment.find('basket_text').start_time.should == experiment_start_time end it "should handle not having a start time" do experiment_start_time = Time.parse("Sat Mar 03 14:01:03") Time.stub(:now => experiment_start_time) experiment.save Split.redis.hdel(:experiment_start_times, experiment.name) Split::Experiment.find('basket_text').start_time.should == nil end it "should not create duplicates when saving multiple times" do experiment.save experiment.save Split.redis.exists('basket_text').should be true Split.redis.lrange('basket_text', 0, -1).should eql(['Basket', "Cart"]) end describe 'new record?' do it "should know if it hasn't been saved yet" do experiment.new_record?.should be_true end it "should know if it has been saved yet" do experiment.save experiment.new_record?.should be_false end end describe 'find' do it "should return an existing experiment" do experiment.save experiment = Split::Experiment.find('basket_text') experiment.name.should eql('basket_text') end it "should return an existing experiment" do Split::Experiment.find('non_existent_experiment').should be_nil end end describe 'control' do it 'should be the first alternative' do experiment.save experiment.control.name.should eql('Basket') end end end describe 'initialization' do it "should set the algorithm when passed as an option to the initializer" do experiment = Split::Experiment.new('basket_text', :alternatives => ['Basket', "Cart"], :algorithm => Split::Algorithms::Whiplash) experiment.algorithm.should == Split::Algorithms::Whiplash end it "should be possible to make an experiment not resettable" do experiment = Split::Experiment.new('basket_text', :alternatives => ['Basket', "Cart"], :resettable => false) experiment.resettable.should be_false end end describe 'persistent configuration' do it "should persist resettable in redis" do experiment = Split::Experiment.new('basket_text', :alternatives => ['Basket', "Cart"], :resettable => false) experiment.save e = Split::Experiment.find('basket_text') e.should == experiment e.resettable.should be_false end it "should persist algorithm in redis" do experiment = Split::Experiment.new('basket_text', :alternatives => ['Basket', "Cart"], :algorithm => Split::Algorithms::Whiplash) experiment.save e = Split::Experiment.find('basket_text') e.should == experiment e.algorithm.should == Split::Algorithms::Whiplash end it "should persist a new experiment in redis, that does not exist in the configuration file" do experiment = Split::Experiment.new('foobar', :alternatives => ['tra', 'la'], :algorithm => Split::Algorithms::Whiplash) experiment.save e = Split::Experiment.find('foobar') e.should == experiment e.alternatives.collect{|a| a.name}.should == ['tra', 'la'] end end describe 'deleting' do it 'should delete itself' do experiment = Split::Experiment.new('basket_text', :alternatives => [ 'Basket', "Cart"]) experiment.save experiment.delete Split.redis.exists('link_color').should be false Split::Experiment.find('link_color').should be_nil end it "should increment the version" do experiment.version.should eql(0) experiment.delete experiment.version.should eql(1) end it "should call the on_experiment_delete hook" do expect(Split.configuration.on_experiment_delete).to receive(:call) experiment.delete end end describe 'winner' do it "should have no winner initially" do experiment.winner.should be_nil end it "should allow you to specify a winner" do experiment.save experiment.winner = 'red' experiment.winner.name.should == 'red' end end describe 'has_winner?' do context 'with winner' do before { experiment.winner = 'red' } it 'returns true' do expect(experiment).to have_winner end end context 'without winner' do it 'returns false' do expect(experiment).to_not have_winner end end end describe 'reset' do before { experiment.save } it 'should reset all alternatives' do experiment.winner = 'green' experiment.next_alternative.name.should eql('green') green.increment_participation experiment.reset green.participant_count.should eql(0) green.completed_count.should eql(0) end it 'should reset the winner' do experiment.winner = 'green' experiment.next_alternative.name.should eql('green') green.increment_participation experiment.reset experiment.winner.should be_nil end it "should increment the version" do experiment.version.should eql(0) experiment.reset experiment.version.should eql(1) end it "should call the on_experiment_reset hook" do expect(Split.configuration.on_experiment_reset).to receive(:call) experiment.reset end end describe 'algorithm' do let(:experiment) { Split::Experiment.find_or_create('link_color', 'blue', 'red', 'green') } it 'should use the default algorithm if none is specified' do experiment.algorithm.should == Split.configuration.algorithm end it 'should use the user specified algorithm for this experiment if specified' do experiment.algorithm = Split::Algorithms::Whiplash experiment.algorithm.should == Split::Algorithms::Whiplash end end describe 'next_alternative' do let(:experiment) { Split::Experiment.find_or_create('link_color', 'blue', 'red', 'green') } it "should always return the winner if one exists" do green = Split::Alternative.new('green', 'link_color') experiment.winner = 'green' experiment.next_alternative.name.should eql('green') green.increment_participation experiment.next_alternative.name.should eql('green') end it "should use the specified algorithm if a winner does not exist" do experiment.algorithm = Split::Algorithms::Whiplash experiment.algorithm.should_receive(:choose_alternative).and_return(Split::Alternative.new('green', 'link_color')) experiment.next_alternative.name.should eql('green') end end describe 'single alternative' do let(:experiment) { Split::Experiment.find_or_create('link_color', 'blue') } it "should always return the color blue" do experiment.next_alternative.name.should eql('blue') end end describe 'changing an existing experiment' do def same_but_different_alternative Split::Experiment.find_or_create('link_color', 'blue', 'yellow', 'orange') end it "should reset an experiment if it is loaded with different alternatives" do experiment.save blue.participant_count = 5 same_experiment = same_but_different_alternative same_experiment.alternatives.map(&:name).should eql(['blue', 'yellow', 'orange']) blue.participant_count.should eql(0) end it "should only reset once" do experiment.save experiment.version.should eql(0) same_experiment = same_but_different_alternative same_experiment.version.should eql(1) same_experiment_again = same_but_different_alternative same_experiment_again.version.should eql(1) end end describe 'alternatives passed as non-strings' do it "should throw an exception if an alternative is passed that is not a string" do lambda { Split::Experiment.find_or_create('link_color', :blue, :red) }.should raise_error lambda { Split::Experiment.find_or_create('link_enabled', true, false) }.should raise_error end end describe 'specifying weights' do let(:experiment_with_weight) { Split::Experiment.find_or_create('link_color', {'blue' => 1}, {'red' => 2 }) } it "should work for a new experiment" do experiment_with_weight.alternatives.map(&:weight).should == [1, 2] end it "should work for an existing experiment" do experiment.save experiment_with_weight.alternatives.map(&:weight).should == [1, 2] end end describe "specifying goals" do let(:experiment) { new_experiment(["purchase"]) } context "saving experiment" do def same_but_different_goals Split::Experiment.find_or_create({'link_color' => ["purchase", "refund"]}, 'blue', 'red', 'green') end before { experiment.save } it "can find existing experiment" do Split::Experiment.find("link_color").name.should eql("link_color") end it "should reset an experiment if it is loaded with different goals" do same_experiment = same_but_different_goals Split::Experiment.find("link_color").goals.should == ["purchase", "refund"] end end it "should have goals" do experiment.goals.should eql(["purchase"]) end context "find or create experiment" do it "should have correct goals" do experiment = Split::Experiment.find_or_create({'link_color3' => ["purchase", "refund"]}, 'blue', 'red', 'green') experiment.goals.should == ["purchase", "refund"] experiment = Split::Experiment.find_or_create('link_color3', 'blue', 'red', 'green') experiment.goals.should == [] end end end end