ruby-on-railsrubymobility

How to setup fixtures when using Mobility with a Key Value backend?


Got a question on how to setup fixtures for Mobility. Would be very grateful for any tips on how to get this going and would be a valuable lesson for me as well on how to tackle setting up fixtures in general.

Not using any gems to setup fixtures, just the default Rails approach for this case. I have a Song model which has multiple translatable attributes, title uses Mobility, description and content use Mobility Action Text.

It works really well but when setting up fixtures I'm finding it difficult to relate the records. There's three tables at play here songs where the only field used is status. mobility_string_translations stores translations for title and action_text_rich_texts stores translated descriptions and content.

This is how my translation setup looks like in Song:

class Song < ApplicationRecord
  extend Mobility

  validates :title_pt, presence: true 
  validates :status, inclusion: { in: %w(draft published private) },
  presence: true

  translates :title, type: :string, locale_accessors: I18n.available_locales
  translates :description, backend: :action_text, locale_accessors: I18n.available_locales
  translates :content, backend: :action_text, locale_accessors: I18n.available_locales
# file continuation...

As for fixtures songs.yml looks like this:

one:
  status: "published"

Then based on what I've found online I've created mobility/string_translations.yml with the following content:

one:
  translatable_id: one (Song)
  translatable_type: "Song"
  key: "title"
  value: "Title in English"
  locale: "en"
  created_at: <%= Time.now %>
  updated_at: <%= Time.now %>
two:
  translatable_id: one (Song)
  translatable_type: "Song"
  key: "title"
  value: "Titulo em Português"
  locale: "pt"
  created_at: <%= Time.now %>
  updated_at: <%= Time.now %>

This seems to work but I know it isn't because when I inspect @song = songs(:one) looking for translated values (@song.title_pt and @song.title_en) they're both nil.

Any idea on what to do here? 🙏


Solution

  • The issue in my case is that translatable_type was Song instead of "Song" and it couldn't map the records in mobility_string_translations to the correct Song record. Here's a bit more detail on the setup that I have that does work to write tests:

    By work I mean, the Mobility translation records defined in fixture files are detected and can be used to compose tests. Running @song.title_en should output a value instead of nil.

    Let's consider the following Song model, it has a title that can be translated and a status which is only used to affect the visibility of the song in the front end. Fixtures for a couple of Songs would look like this:

    # test/fixtures/songs.yml
    one:
        id: 1
        status: "published"
    
    two:
        id: 2
        status: "draft"
    

    The id is usually not specified in fixtures but here it becomes necessary so that we're sure which identifier to use when pointing translated records.

    The Mobility implementation will store any translated titles at mobility_string_translations the following can be added to test/fixtures/mobility/string_translations.yml:

    # test/fixtures/mobility/string_translations.yml
    song_one_en:
        translatable_id: 1
        translatable_type: "Song"
        key: "title"
        value: "Maçaranduba Wood"
        locale: "en"
        created_at: <%= Time.now %>
        updated_at: <%= Time.now %>
    song_one_pt:
        translatable_id: 1
        translatable_type: "Song"
        key: "title"
        value: "Madeira de Maçaranduba"
        locale: "pt"
        created_at: <%= Time.now %>
        updated_at: <%= Time.now %>
    song_two_en:
        translatable_id: 2
        translatable_type: "Song"
        key: "title"
        value: "Dona Maria from Camboatá"
        locale: "en"
        created_at: <%= Time.now %>
        updated_at: <%= Time.now %>
    song_two_pt:
        translatable_id: 2
        translatable_type: "Song"
        key: "title"
        value: "Dona Maria do Camboatá"
        locale: "pt"
        created_at: <%= Time.now %>
        updated_at: <%= Time.now %>
    

    Each song includes a title for English and Portuguese in this case but any locales the record is going to make use of or need to be tested can be included here, in an individual record.

    The important aspect here is that all translatable_type columns are explicit string types. For example, do "Song", instead of Song when adding a value to the property.

    Setting up fixtures with this method associates translated properties to a record and enables them to be accessed in a test.

    For example, to change the title of a song, the record can be brought into the test in a setup block and the title translations will be available and can be modified:

    # test/controllers/song_controller_test.rb
    require "test_helper"
    
    class SongControllerTest < ActionDispatch::IntegrationTest
        setup do
            @song = songs(:one)
        end
    
        test "admin can edit a song" do
            # Keeps a copy of the original record for comparison.
            current_record = @song
            
            # Passes the locale to the request helper to keep it from getting confused with the record id.
            # Changes the title of the record.
            patch song_url(I18n.locale, @song, { song: { title_en: 'Updated Song Title' } })
            
            # Retrieves the same record to be used for comparison.
            updated_record = Song.find(@song.id)
            
            # Checks that a change actually occurred.
            assert current_record.updated_at != updated_record.updated_at
            
            # Checks that the list of songs is being displayed to the user.
            assert_redirected_to songs_path
        end
    end
    

    To make sure that the fixture has setup the association bettween the model and the translated records, the debugger method can be used. Start by adding it as a breakpoint to your test logic, in this case I'm going to use the example above:

    # test/controllers/song_controller_test.rb
    test "admin can edit a song" do
        current_record = @song  
        patch song_url(I18n.locale, @song, { song: { title_en: 'Updated Song Title' } })
        updated_record = Song.find(@song.id)
        
        debugger # <-- The script will pause here.
        
        assert current_record.updated_at != updated_record.updated_at
        assert_redirected_to songs_path
    end
    

    Then the test can be run, bin/rails test would work but in this example the command to run just the tests for this file would be:

    bin/rails test test/controllers/role_controller_test.rb
    

    The output in the terminal will look similar to this, the program will be paused at this point and it is interactive:

    bin/rails test test/controllers/song_controller_test.rb
    Running 26 tests in a single process (parallelization threshold is 50)
    Run options: --seed 56548
    
    # Running:
    .............[64, 73] in ~/Projects/rails_app/test/controllers/song_controller_test.rb
    64|
    65| current_record = @song
    66| patch song_url(I18n.locale, @song, { song: { title_en: 'Updated Song Title' } })
    67| updated_record = Song.find(@song.id)
    68|
    => 69| debugger # <-- The script will pause here.
    70|
    71| # Checks that a change actually occurred.
    72| assert current_record.updated_at != updated_record.updated_at
    73|
    =>
    
    #0 block in <class:SongControllerTest> at ~/Projects/rails_app/test/controllers/song_controller_test.rb:69
    
    #1 block in run (3 levels) at ~/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.15.0/lib/minitest/test.rb:98
    
    # and 24 frames (use `bt' command for all frames)
    
    (rdbg)
    

    Any variables defined before debugger can be accessed, this can be used to inspect if @song was changed:

    (rdbg) @song.title_en    # ruby
    "Updated Song Title"
    (rdbg) @song.title_pt    # ruby
    "Madeira de Maçaranduba"
    

    The title was updated using the patch request defined in the test case. Typing continue will move on from the breakpoint and continue running the code in the test file.

    That should be it!