Forum: Ruby on Rails STI and Fixtures

Announcement (2017-05-07): www.ruby-forum.com is now read-only since I unfortunately do not have the time to support and maintain the forum any more. Please see rubyonrails.org/community and ruby-lang.org/en/community for other Rails- und Ruby-related community platforms.
Phoenix R. (Guest)
on 2009-05-11 21:07
(Received via mailing list)
I'm trying to implement a TDD design pattern in Rails for the first
time for a system maintenance reporting application, and running into
issues with STI (also a first-time user) and fixtures causing my tests
to throw errors.

class Report < ActiveRecord::Base
  validates_presence_of :subject, :author, :category_id
  validates_inclusion_of :status, :in => [true, false]
  validates_inclusion_of :type, :in => ["MaintenanceReport",
"OutageReport"]
end

"Report" is the parent class I'm implementing.  Each report - be it a
maintenance report or an outage report - has the following:

(This is also my first fixture: reports.yml)
----------------------------------------
sample_report:
  type: MaintenanceReport
  subject: Sample Maintenance Report
  affected_systems: "System 1, System 2"
  author: Some Author
  status: false
  category_id: 1
  misc_notes: "This is a test.  Disregard."
  finish_time: <%= Time.now %>
  est_resolution_time: 3 days

By design, every report should be either a MaintenanceReport or an
OutageReport, meaning that if I simply strike "type" from my fixture
above, the test to make sure it's one of those will fail.

My MaintenanceReport model:
------------------------------------------
class MaintenanceReport < Report

validates_presence_of :risks, :backout_plan, :description, :work_by,
:scheduled_start_time
end

Now, here's where the rubber meets the road:

When running the actual tests, they all fail because it's trying to
insert values into columns that don't exist.  For example:

ActiveRecord::StatementInvalid: Mysql::Error: Unknown column
'est_resolution_time' in 'field list': INSERT INTO
`maintenance_reports` (`est_resolution_time`, [...snip]

It's inserting into maintenance_reports when it should insert into
reports for this specific test (because it's in reports.yml, correct?)

For the curious, that specific test is:
---------------
(report_test.rb)
  def test_report_validations
    report = Report.new

    # Report shouldn't save because it has no data for validations
    assert !report.save

    # Give it a type
    report.type = "MaintenanceReport"
    assert !report.save

    # Subject
    report.subject = "Test Report"
    assert !report.save

    # Author
    report.author = "Some Author"
    assert !report.save

    # Status
    report.status = false
    assert !report.save

    # Category ID
    report.category_id = 1

    # all required methods are now supplied, should save
    assert report.save

  end

------------------------

As you can see here, every report MUST have a type, so that's set, and
it MUST be either MaintenanceReport or OutageReport (also set); this
is supposed to test only the Report class unto itself before any
testing of the MaintenanceReport class and its specific methods occurs
(logically speaking, it shouldn't be necessary to test the same
methods in both classes).

The reverse is also an issue:
----------------------------------------
(fixtures/maintenance_reports.yml)
good_maintenance_report:
  type: MaintenanceReport
  subject: Sample Maintenance Report
  affected_systems: "System 1, System 2"
  author: Some Author
  status: false
  category_id: 1
  misc_notes: "This is a test.  Disregard."
  finish_time: <%= Time.now %>
  est_resolution_time: 3 days
  risks: "This test might fail!"
  backout_plan: "None - fix the problem and make the tests pass"
  description: "test description"
  work_by: Some P.
  scheduled_start_time: <%= Time.now %>

---
(unit/maintenance_report_test.rb)
[snip]

  def test_create_validate_save
    report = maintenance_reports(:good_maintenance_report)

    # By default this report is complete, so it should save.
    assert report.save

    # Remove a validated attribute
    report.risks = nil
    assert !report.save
    report = maintenance_reports(:good_maintenance_report) # Reset

    # Delete backout plan, test, reset
    report.backout_plan = nil
    assert !report.save
    report = maintenance_reports(:good_maintenance_report)

    # Delete description, test, reset
    report.description = nil
    assert !report.save
    report = maintenance_reports(:good_maintenance_report)

    # Delete work by, test, reset
    report.work_by = nil
    assert !report.save
    report = maintenance_reports(:good_maintenance_report)

    # Delete scheduled start time, test, reset
    report.scheduled_start_time = nil
    assert !report.save
    report = maintenance_reports(:good_maintenance_report)

    # Test save again
    assert report.save
  end

--------

  1) Error:
test_create_validate_save(MaintenanceReportTest):
ActiveRecord::StatementInvalid: Mysql::Error: Unknown column
'est_resolution_time' in 'field list': INSERT INTO
`maintenance_reports` (`est_resolution_time`, `created_at`,
`backout_plan`, `author`, `updated_at`, `affected_systems`, `id`,
`work_by`, `subject`, `risks`, `category_id`, `type`, `finish_time`,
`misc_notes`, `description`, `status`, `scheduled_start_time`) VALUES
('3 days', '2009-05-11 16:59:47', 'None - fix the problem and make the
tests pass', 'Some Author', '2009-05-11 16:59:47', 'System 1, System
2', 525138997, 'Some P.', 'Sample Maintenance Report', 'This test
might fail!', 1, 'MaintenanceReport', 'Mon May 11 10:59:47 -0600
2009', 'This is a test.  Disregard.', 'test description', 0, 'Mon May
11 10:59:47 -0600 2009')

-------
So the obvious next question is, "what's your database look like?"
answer:

class CreateReports < ActiveRecord::Migration
  def self.up
    create_table :reports do |t|
      t.string        :type
      t.string        :subject
      t.text          :affected_systems
      t.string        :author
      t.string        :closer
      t.boolean       :status
      t.integer       :category_id
      t.text          :misc_notes
      t.datetime      :finish_time
      t.string        :est_resolution_time
      t.timestamps
    end
  end

  def self.down
    drop_table :reports
  end
end


class CreateMaintenanceReports < ActiveRecord::Migration
  def self.up
    create_table :maintenance_reports do |t|
      t.text          :risks
      t.text          :backout_plan
      t.text          :description
      t.string        :work_by
      t.datetime      :scheduled_start_time
      t.timestamps
    end
  end

  def self.down
    drop_table :maintenance_reports
  end
end

I've googled and read the API documentation for two days looking for
any examples as to how I can tell Rails that my fixtures are based off
STI.  Can anyone offer any input on this?
Frederick C. (Guest)
on 2009-05-11 22:19
(Received via mailing list)
On May 11, 6:06 pm, Phoenix R. <removed_email_address@domain.invalid> wrote:
>
>
> I've googled and read the API documentation for two days looking for
> any examples as to how I can tell Rails that my fixtures are based off
> STI.  Can anyone offer any input on this?

Fixtures are a bit odd - they're more table based than class based
(sortof - hence the odd). If you stick everything in reports.yml you
should be ok.

Fred
Marnen L. (Guest)
on 2009-05-12 00:36
Frederick C. wrote:
[...]
> Fixtures are a bit odd - they're more table based than class based
> (sortof - hence the odd). If you stick everything in reports.yml you
> should be ok.

Or just use factories.  At least with Machinist, they seem to work
directly with the class structure.  I've never used STI with Machinist,
but I'd be *very* surprised, given Machinist's architecture, if it
didn't work as expected, without the gotchas of fixtures.

Best,
--
Marnen Laibow-Koser
http://www.marnen.org
removed_email_address@domain.invalid
This topic is locked and can not be replied to.