Search code examples
ruby-on-railscucumberfactory-botruby-on-rails-5faker

FactoryGirl factory that may reuse existing models as association


Suppose I want to run a test and create many Students. Each student belongs to one school. The School names are given by the Faker gem, but there's a limited number of them, and students are associated to those schools.

Can I use FactoryGirl in a way that lets me reuse existing schools ? ie. a new FactoryGirl.create(:student)is assigned to either

  • a new school not yet faked => school is faked with FactoryGirl.create(:school)
  • or to an already existing faked school => School is already faked and just retrieved from the DB

.

class Student
  belongs_to :school, class_name: 'SchoolSociety'
end

class School
  has_many :students
  field :name
end

I believe it's irrelevant here, but I'm using Mongoid. My factories would look like

FactoryGirl.define do
  factory :student, class: Student do
    association(:school, factory: :school)
  end
  factory :school, class: School do 
    name { Faker::University.name }
  end
end

One solution would be to use School.where(name: Faker::University.name) but I'd lose all the flexibility of FactoryGirls factories... Any better workaround ?

The context is running Cucumber tests with many students

Edit:

My actual cucumber case is testing a jackpot that increases according to a math formula involving current time and number of registered users (that registered under a School). I was doing something in the likes of

Scenario Outline: Jackpot increases with registrations and time
    Given the current date is <date>
    And <count> students have registered for the special event
    When I am on the special event page
    Then I should see "<jackpot> €"

    Examples:
    | date | count | jackpot |
    | 2016/11/24 15:00:00 |  5 | 4 9 5 , 3 0 |
    | 2016/11/30 15:00:00 |  10 | 4 9 7 , 6 0 |
    | 2016/12/10 15:00:00 |  20 | 5 0 2 , 2 0 |
    | 2016/12/10 15:00:00 |  150 | 6 5 2 , 2 0 |

Now, those <count> students have registered for the special event must be students that belong to an existing school (which means, during the registration process, they must register with a school email whose domain exists/is mappable to a School in our DB)


Solution

  • You should be specific and precise about your Givens when using Cucumber rather than letting factory rules decide what is created. So if you want to create several students in the same school I would have

    Given there is a school with several students

    If you want to have several schools with students

    Given there are several schools with students

    And if you want to have a student that is enrolled in more than one school

    Given there is a student enrolled in two schools

    Obviously you can create a create deal of variety here by doing things like

    1. naming schools
    2. naming students
    3. varying quantities

    When you implement these steps, especially when you are starting, its tempting to put all the code to do the work in the step. Resist this, instead make calls in the steps and make the calls match the step description. For example

     Given 'there is a school with several students' do 
      @school = create_school
      @students = []
      several.times do 
        @students << create_student school: school
      end
    end
    

    This step uses two methods create_school and create_student. You would define these in a helper module

    module SchoolsStepHelper
      def create_school
        ...
       
      def create_student(school: ...)
        ...
    end
    World SchoolsStepHelper
    

    Now you can be precise about how you create your students, and when you come across something new e.g. a student enrolled in two schools you can add/modify your methods to get this extra functionality, e.g. you might add

    def enroll_student(student: school:)
      ...
    

    so that we can do

    Given 'there is a student enrolled in two schools' do
      @student = create_student
      @school_1 = create_school
      @school_2 = create_schoo
      enroll_student(student: @student, school: @school_1)
      enroll_student(student: @student, school: @school_1)
    end
    

    Now however we still have too much code in our step definitions, so we need to refactor. Here we have a couple of choices

    1. Break this compound step into simpler steps so that the student and schools already exist e.g.
        Given there is a school harvard
        And there is a school yale
        And there is a student Fred
        And Fred is enrolled in yale and harvard.
    

    You'd do things this way to make your scenario more descriptive, particularly when developing the enroll functionality

    1. Extract a higher level method
        Given 'there is a student enrolled in two schools' do
          @student = create_dual_enrolled_student
        end
    

    Which ever approach you chose, you will be reusing the simple methods we created at the beginning of this answer.

    Now your question about Factories is basically an implementation detail, about how you create things. It should be possible to

    1. Implement a solution which requires little or no understanding of factories to understand whats happening. (just very simple Factory calls in your methods)

    2. Implement a solution that doesn't even use factories (this is the approach I favour, but thats a whole other story).

    Finally if you take this approach and all your step definitions are implemented as simple calls, then it doesn't matter if you have lots of similar step definitions e.g.

        Given there is a student
        Given Fred is a student
        Given there is a student Fred
        Given there is a student Sue
    

    You can have a step definition for each one of these without creating duplication because making calls is not duplication and the cost of the extra steps is just about balanced by the simplicity of implementation and the need for no parameters or regexs.

        Given 'there is a student' do
          create_student
        end
        Given 'Fred is a student' do
          create_student name: 'Fred'
        end
        Given there is a student Fred do
          create_student name: Fred
        end
        Given 'there is a student Sue' do
          create_student name: 'Sue'
        end
    

    Phew that was a long answer, I hope its useful.