Skip to content

Avoid testing (and production) problems with Postqresql

Patrick Bolger edited this page Nov 17, 2017 · 7 revisions

This is an overview of "lessons learned" when we converted our dev and test DB from sqlite3 to postgresql (April, 2017). These provide lessons in how to avoid certain problems when testing against postgresql. These problems arose, generally, from the fact that sqlite3 - not being a robust DB - lets us get away with sloppy testing practices that postgresql does not.

User dependent: :destroy correctly with through: associations

For these types of associations, make sure the dependent: :destroy option is specified on the through: association, not the association between the owner model and the join table.

Example: This is from the Job model. It has many job-related questions, and these are accessed via a join table representing the JobQuestions model. Here, we want to delete associated records from the join table if the the owning job is destroyed.

Wrong:

has_many :job_questions, inverse_of: :job, dependent: :destroy
has_many :questions, through: :job_questions

Although this might appear logical, since we want to delete a join table record and thus specifying the dependent: :destroy option on that association would seem to make sense, it is not correct.

Correct:

has_many :job_questions, inverse_of: :job
has_many :questions, through: :job_questions, dependent: :destroy

In effect, we're telling Rails to delete the through record when the job is destroyed.

NOTE: When destroying the owning object, be sure to use the destroy method rather than the delete method. The former allows callbacks to execute, and the dependent: :destroy action uses a callback to destroy the associated join table record.

Do not assume any specific ordering when fetching unordered collections.

sqlite3 seems to provide unordered records in a specific order (ascending ID) every time, whereas postgresql does not (Rails does not guarantee any specific order when fetching unordered collections of records).

For instance, this code tests that job seekers assigned to a job developer are assigned to an instance variable in the controller action:

it 'assigns job seekers to the job developer' do
  patch :update, id: jd_person, agency_person: person_hash
  expect(assigns(:agency_person).as_jd_job_seeker_ids)
    .to eq [job_seeker.id, adam.id]
end

The action is not fetching the records in any order, yet the test assumes a fixed ordering every time.

Instead, this test for the same assignment but does not assume any ordering in the fetched records:

it 'assigns job seekers to the job developer' do
  patch :update, id: jd_person, agency_person: person_hash
  expect(assigns(:agency_person).as_jd_job_seeker_ids)
    .to contain_exactly(job_seeker.id, adam.id)
end

Here is another example using a JSON structure:

Do not do this:

expect(JSON.parse(subject.body))
  .to eq('results' =>
              [
                { 'id' => @jd1.id,
                  'text' => @jd1.full_name },
                { 'id' => @jd2.id,
                  'text' => @jd2.full_name },
                { 'id' => @jd3.id,
                  'text' => @jd3.full_name },
                { 'id' => @jd4.id,
                  'text' => @jd4.full_name }
               ])

Do this instead:

results = JSON.parse(subject.body)
expect(results).to include('results')
expect(results['results'])
  .to include({ 'id' => @jd1.id,
                'text' => @jd1.full_name },
              { 'id' => @jd2.id,
                'text' => @jd2.full_name },
              { 'id' => @jd3.id,
                'text' => @jd3.full_name },
              'id' => @jd4.id,
              'text' => @jd4.full_name)

Order collections in your code if required for test purposes

Generally we don't want to write code that is tailored to ease of testing. However, in certain circumstances this might be viable if the cost is not great and it makes for cleaner and straight-forward test statements.

For instance, this test code assumes a certain ordering for the method parameter notify_list:

expect(Pusher).to have_received(:trigger)
  .with('pusher_control',
        'job_posted',
        job_id:    job.id,
        job_title: job.title,
        company_name: company.name,
        notify_list: [job_developer.user.id, job_developer1.user.id])

In the code that actually calls the tested method, the collection is sorted:

jd_ids = job_developers.map { |jd| jd.user.id }.sort   # sort for test purposes

Note the comment in the code - this is important because a reader may not understand why the collection is being sorted.

Clone this wiki locally