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 description
s 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? 🙏
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 ofnil
.
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 title
s 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 explicitstring
types. For example, do"Song"
, instead ofSong
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!