factory_girl 创建重复值

factory_girl creates duplicate value

rails 4.1.1
rspec3.2.1

宝石文件

source 'https://rubygems.org'

# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
gem 'rails', '4.1.1'
# Use mysql as the database for Active Record
gem 'mysql2', '~> 0.3.13', :platform => :ruby
gem 'jdbc-mysql', '5.1.28', :platform => :jruby

# Use SCSS for stylesheets
gem 'sass-rails', '~> 4.0.3'
# Use Uglifier as compressor for JavaScript assets
gem 'uglifier', '>= 1.3.0'
# Use CoffeeScript for .js.coffee assets and views
gem 'coffee-rails', '~> 4.0.0'
# See https://github.com/sstephenson/execjs#readme for more supported runtimes
# gem 'therubyracer',  platforms: :ruby

# Use jquery as the JavaScript library
gem 'jquery-rails'
# Turbolinks makes following links in your web application faster. Read more: https://github.com/rails/turbolinks
gem 'turbolinks'
gem 'jquery-turbolinks'
# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
gem 'jbuilder', '~> 2.0'
# bundle exec rake doc:rails generates the API under doc/api.
gem 'sdoc', '~> 0.4.0',          group: :doc

# Use ActiveModel has_secure_password
# gem 'bcrypt', '~> 3.1.7'

# Use unicorn as the app server
# gem 'unicorn'

# Use Capistrano for deployment
# gem 'capistrano-rails', group: :development

# Use debugger
# gem 'debugger', group: [:development, :test]

# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem 'tzinfo-data', platforms: [:mingw, :mswin]

gem 'slim-rails'
gem 'devise'
gem 'cancan'
gem 'rakismet'

gem 'carrierwave'
gem 'rmagick', :require => false
gem 'faker', '1.0.1'

gem 'elasticsearch-model'
gem 'elasticsearch-rails'
gem 'bonsai-elasticsearch-rails'

gem 'remotipart', '~> 1.2'
gem 'kaminari'
gem 'thin'
gem 'carrierwave-aws'

group :production do
  gem 'rails_12factor', '0.0.2'
end

gem 'faye-rails', '~> 2.0'

group :development, :test do
  gem 'rspec-rails', '~> 3.0'
  gem 'factory_girl_rails'
end

group :test do
  gem 'capybara'
  gem 'database_cleaner'
  gem 'launchy'
  gem 'selenium-webdriver'
  gem "shoulda-matchers", "~> 2.2.0"
end

spec_helper.rb

ENV["RAILS_ENV"] ||= 'test'
require File.expand_path("../../config/environment",__FILE__)
require 'rspec/rails'
require "capybara/rspec"
require 'shoulda/matchers'

Dir[Rails.root.join("spec/support/**/*.rb")].each {|f| require f}
RSpec.configure do |config|
  config.include Devise::TestHelpers, :type => :controller
  config.extend ControllerMacros, :type => :controller
  config.include MailerMacros
  config.include Capybara::DSL, :type => :feature

  config.use_transactional_fixtures = false

  config.before :each do
    if Capybara.current_driver == :selenium
      DatabaseCleaner.strategy = :truncation
    else
      DatabaseCleaner.strategy = :transaction
    end
    DatabaseCleaner.start
  end

  config.after(:each) do
    DatabaseCleaner.clean
  end

  config.expect_with :rspec do |expectations|
    expectations.include_chain_clauses_in_custom_matcher_descriptions = true
  end

  config.mock_with :rspec do |mocks|
    mocks.verify_partial_doubles = true
  end

  # Include Factory Girl syntax to simplify calls to factories
  config.include FactoryGirl::Syntax::Methods

# The settings below are suggested to provide a good initial experience
# with RSpec, but feel free to customize to your heart's content.

结束

factories/tickets.rb

FactoryGirl.define do
  factory :ticket do
    customer_name { Faker::Name.name }
    customer_email Faker::Internet.email
    subject Faker::Lorem.sentence
    body Faker::Lorem.sentence

    association :department

    permalink "/tickets/token/AAA-AAA-AAA-AAA-AAA" 
    remote_ip "127.0.0.1"
    user_agent "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:34.0) Gecko/20100101 Firefox/34.0"
    referrer "http://127.0.0.1:3000/"    
  end

  factory :spam_ticket, parent: :ticket do
    body "viagra-123"
  end

  factory :invalid_ticket, parent: :ticket do
    customer_name nil
    customer_email nil
  end
end

tickets_controller_spec.rb

require 'spec_helper'

describe TicketsController, :type => :controller do 
  let(:department) { create(:department) }

  let!(:tickets) do
    (1..5).collect { create(:ticket, department: department) } +
      [create(:ticket, department: nil), create(:ticket, department: create(:department))]
  end

  let(:ticket) { tickets.first }
  let(:invalid_ticket) { build(:invalid_ticket) }
  let(:valid_attributes) { attributes_for(:ticket) }
  let(:invalid_attributes) { attributes_for(:invalid_ticket) }
  let(:spam_attributes) { attributes_for(:spam_ticket) }

  describe "GET #index" do
    context "not signed user" do
      it "list of tickets should redirect" do
        get :index
        expect(response).to redirect_to(new_staff_session_path)
      end
    end        

    context 'signed as member who is allowed to view tickets applied only to his department' do
      login_as 'member'

      it "populates an array of tickets of a particular department" do
        @current_user.update(department: ticket.department)

        get :index 
        expect(assigns(:tickets)).to eq tickets[0...-1]
      end

      it "renders the :index template" do
        get :index
        expect(response).to render_template :index
      end
    end  

    context 'signed as admin who is allowed to view all tickets' do
      login_as 'admin'

      it "populates an array of tickets of all departments" do
        get :index 
        expect(assigns(:tickets)).to match_array(tickets)
      end
    end
  end

  describe "GET #new" do
    before { get :new }
    it "assigns a new Ticket to @ticket" do
      expect(assigns(:ticket)).to be_a_new(Ticket)
    end

    it "renders the :new template" do
      expect(response).to render_template :new
    end
  end

  describe "GET #edit" do
    before { get :edit, token: ticket.token }

    it "assigns the requested contact to @ticket" do
      expect(assigns(:ticket)).to eq ticket
    end

    it "render the #edit template" do
      expect(response).to render_template :edit
    end
  end

  describe "POST #create" do
    context "with valid attributes" do
      it "saves the new ticket in the database" do
        expect{
          post :create, ticket: valid_attributes
        }.to change(Ticket, :count).by(1)
      end

      it "redirects to tickets#show" do
        post :create, ticket: valid_attributes
        expect(response).to redirect_to show_tickets_path(assigns[:ticket].token)
      end
    end

    context "with invalid attributes" do
      before { 
        post :create, ticket: invalid_attributes 
      }

      it "does not save the new ticket in the database" do
        t = Logger.new(STDOUT)
        t.debug tickets
        t.debug '=============================================='
        t.debug create(:ticket)
        t.debug create(:ticket)
        t.debug build(:invalid_ticket)
        t.debug '=============================================='

        expect(Ticket.exists?(body: invalid_ticket[:body])).to be_false
      end

      it "renders the :new template" do
        expect(response).to render_template :new
      end
    end

    context "with spam content" do
      before { post :create, ticket: spam_attributes }

      it "throws out the error" do
        expect(assigns(:ticket).spam?).to be_truthy
      end

      it "renders the :new template" do
        expect(response).to render_template :new
      end
    end
  end

  describe "PATCH #update" do
    context "with valid attributes" do
      it "locates the requested @ticket" do
        patch :update, token: ticket.token, ticket: valid_attributes
        expect(assigns(:ticket)).to eq(ticket)
      end

      it "changes @ticket's attributes" do
        patch :update, token: ticket.token, ticket: attributes_for(:ticket, customer_name: "John Johnson")
        ticket.reload
        expect(ticket.customer_name).to eq("John Johnson")
      end

      it "redirects to the updated ticket" do
        patch :update, token: ticket.token, ticket: valid_attributes
        expect(response).to redirect_to(show_tickets_path(ticket.token))
      end
    end

    context "with invalid attributes" do
      it "does not change ticket's attributes" do
        patch :update, token: ticket.token, ticket: attributes_for(:ticket, customer_name: nil, body: "Help!")
        ticket.reload
        expect(ticket.body).to_not eq("Help!")
      end

      it "renders the :edit template" do
        patch :update, token: ticket.token, ticket: attributes_for(:invalid_ticket)
        expect(response).to render_template :edit
      end
    end
  end

  describe "GET #take" do
    login_as 'member'

    it "assigns ticket to a particular staff member" do
      xhr :get, :take, id: ticket
      expect(ticket.reload.taken_tickets).to eq(@current_user) 
    end
  end
end

我得到了意外的输出:
调试 -- : [#http://127.0.0.1:3000/], created_at: "2015-03-26 15:14:58", updated_at: "2015-03-26 15:14:58", department_id: 353, status_id: nil, staff_id: 200, taken_staff_id: nil>, #http://127.0.0.1:3000 /", created_at: "2015-03-26 15:14:58", updated_at: "2015-03-26 15:14:58", department_id: 353, status_id: nil, staff_id: 200, taken_staff_id: nil>, #http://127.0.0.1:3000/", created_at: "2015-03-26 15:14:58", updated_at: "2015-03-26 15:14:58", department_id: 353, status_id: 无, staff_id: 200, taken_staff_id: nil>, #http://127.0.0.1:3000/", created_at: "2015-03-26 15:14:58", updated_at: "2015 -03-26 15:14:58", department_id: 353, status_id: 无, staff_id: 200, 采取_staff_id: 无>, #http:// 127.0.0.1:3000/", created_at: "2015-03-26 15:14:58", updated_at: "2015-03-26 15:14:58", department_id: 353, status_id: nil, staff_id: 200, taken_staff_id: nil>, #http://127.0.0.1:3000/", created_at: " 2015-03-26 15:14:58", updated_at: "2015-03-26 15:14:58", department_id: 无, status_id: 无, staff_id: 200, taken_staff_id: nil>, #http://127.0.0.1:3000/", created_at: "2015-03-26 15:14:58", updated_at: "2015-03-26 15:14:58", department_id: 354, status_id: 无, staff_id: 200, taken_staff_id: 无>] D, [2015-03-26T11:14:58.925686 #7515] 调试 -- : =============================== =============== D、[2015-03-26T11:14:58.934540 #7515] 调试 -- : #http://127.0.0.1:3000/", created_at: "2015-03-26 15:14:58" , updated_at: "2015-03-26 15:14:58", department_id: 355, status_id: 无, staff_id: 200, taken_staff_id :无> D, [2015-03-26T11:14:58.944844 #7515] 调试 -- : #http://127.0.0.1:3000/", created_at: "2015-03-26 15:14:58" , updated_at: "2015-03-26 15:14:58", department_id: 356, status_id: 无, staff_id: 200, taken_staff_id :无> D、[2015-03-26T11:14:58.949122 #7515] 调试 -- : #http://127.0.0.1:3000/", created_at: 无, updated_at: 无, department_id: 357, status_id: 无, staff_id: 无, taken_staff_id: 无> D, [2015-03-26T11:14:58.949449 #7515] 调试 -- : =============================== ===============

你可以看到 3 行代码:

t.debug create(:ticket)
t.debug create(:ticket)
t.debug build(:invalid_ticket)

输出相同body: "Deserunt et accusantium totam ab autem ad."

为什么会出现以及如何解决?

因为Faker::sentence依赖于rand(6) 请看以下代码段:

2.1.5 :001 > rand(6)
 => 5 
2.1.5 :002 > rand(6)
 => 3 
2.1.5 :003 > rand(6)
 => 3 
2.1.5 :004 > rand(6)
 => 4 
2.1.5 :005 > rand(6)
 => 3 
2.1.5 :006 > rand(6)
 => 1 

使用 6 默认值 Faker::Lorum.sentence 使用的

获得相同种子的机会非常高。

这是Faker的来源:

def sentence(word_count = 4, supplemental = false, random_words_to_add = 6)
  words(word_count + rand(random_words_to_add.to_i).to_i, supplemental).join(' ').capitalize + '.'
end

你总是得到同样的句子,这只是运气不好。 您可以做的是覆盖参数并获得更高的种子值以增加随机化。

问题出在factories/tickets.rb.
而是将 Ticket 工厂设置为

factory :ticket do
customer_name { Faker::Name.name }
customer_email Faker::Internet.email
subject Faker::Lorem.sentence
body Faker::Lorem.sentence

您应该使用大括号将值传递给属性:

customer_name { Faker::Name.name }
customer_email { Faker::Internet.email }
subject { Faker::Lorem.sentence }
body { Faker::Lorem.sentence