How can I put a Postgres JSON value into a Rails fixture? - ruby-on-rails

I just added a Postgres json type to a Rails/Active Record table I'm working with.
I'd like to populate a record with a default value in Rails fixtures:
fixture_id:
existing_column: "foobar"
newly_added_column: <%= JSON.dump({:reason => 'foobar'}) %>
Previously, I'd stored stringified JSON this way in a text column. However, when I now run this unit test:
test "my test" do
sut = fixtures(:fixture_id)
assert_not_nil sut.newly_added_column
end
The test fails. Because it is JSON at the database level, I don't think it's useful to dump it to a string, but the YAML fixtures can't seem to keep an object as a Hash (when I try it without the JSON.dump, I get ActiveRecord::Fixture::FormatError: a YAML error occurred parsing).
Mind you, I am using Rails 3, so I think some of the support for this may be in Rails 4, but in Rails 3, the migration to add a json Postgres column type still work.

I believe your fixture can be as simple as:
fixture_id:
existing_column: foobar
newly_added_column: {'reason':'foobar'}

To avoid having lengthy JSON structure inline in your fixtures, YAML provide a useful > operator
fixture_id:
existing_column: "foobar"
data: >
{
"can_edit": true,
"can_se": true,
"can_share": true,
"something_else": true
}

In Rails 5 it is possible to simply describe the JSON in YAML format. It will then be converted into correct JSON when fixtures are loaded in the DB:
parent:
name: A fixture
json_field:
- name: JSON Object 1
foo: bar
- name: JSON Object 2
foo: baz
(tested with JSONB attributes in Postgres)

If you are using ActiveRecord to generate content for your fixtures, you have to ensure that it converts attribute values to json. If you rely on the default serialization, it will produce hash rocket text which is invalid YAML.
For example, I have a legacy database that I am using to create fixtures for my new application (relevant code listed below):
presenter.rb
module FixturePresenter
def replacer(str)
str.downcase.tr(' ', '_').tr('-', '_')
end
# rubocop:disable Metrics/AbcSize
def plan
tag = replacer(name) + '_' + replacer(type)
<<~FIXTURE
#{tag}:
name: #{name}
type: #{type}
address: #{address}
FIXTURE
end
end
runner.rb
...
Legacy::Provider.all.each_with_index do |p, i|
p.extend(FixturePresenter)
f.puts p.plan
end
...
The address column in the plan table is a JSONB datatype. When we run this code this is the resulting YAML:
abc_plan_la_jolla:
name: ABC Plan La Jolla
type: default
address: {"street"=>"9888 Genesee Ave","unit"=>"","city"=>"La Jolla","state"=>"CA","postal_code"=>"92037"}
When you run your test you will get the very sad Syck error:
Psych::SyntaxError: (<unknown>): did not find expected ',' or '}' while parsing a flow mapping at line 4 column 12
The following change to the code will produce the correct YAML:
presenter.rb
address: #{address} to address: #{address.to_json}
The above change will produce a happy fixture definition:
abc_plan_la_jolla:
name: ABC Plan La Jolla
type: default
address: {"street":"9888 Genesee Ave","unit":"","city":"La Jolla","state":"CA","postal_code":"92037"}

Try
fixture_id:
existing_column: "foobar"
newly_added_column: "{\"reason\": \"foobar\"}"

I was maintaining a legacy Rails 4.2 application and needed to put JSON store value into a fixture. I made the following monkey patch to fix the problem. Hope this helps someone:
module ActiveRecord
class Fixture
def to_hash
h = fixture
model_class.attribute_names.each do |name|
typedef = model_class.type_for_attribute(name)
h[name] = typedef.coder.dump(h[name]) \
if typedef.is_a? ActiveRecord::Type::Serialized
end
h
end
end
end

Related

RSpec equality matcher failing for serializer test

I am writing a test for one of my Active Model Serializers to make sure that the JSON output is what I expect. However, I cannot figure out why RSpec is parsing my 'expected' output to leave out my array of testjobs, and I do not understand why I cannot get 'expected' and 'got' outputs to equal each other. At one point, I even copy-pasted the 'got' result to my 'expected' input and still received a failure message that the two strings were not equal. However, when I compared those two strings in REPL using ==, the output was true. How do I resolve these issues to get an effective test?
RSpec Error:
Failures:
1) TestrunSerializer creates special JSON for the API
Failure/Error: expect(serializer.to_json).to eq('{"testrun":{"id":1,"run_at":null,"started_at":null,"state":"pending","completed_at":null,"testjobs":[{"id":2,"active":false,"testchunk_id":2,"testrun_id":1,"testchunk_name":"flair","testchunk":{"id":15,"name":"flair"}}],"branch":{"id":1,"name":"dev","repository":{"id":321,"url":"fakeurl.com"}}}}')
expected: "{\"testrun\":{\"id\":1,\"run_at\":null,\"started_at\":null,\"state\":\"pending\",\"completed_at\":nu...r\"}}],\"branch\":{\"id\":1,\"name\":\"dev\",\"repository\":{\"id\":321,\"url\":\"fakeurl.com\"}}}}"
got: "{\"testrun\":{\"id\":1,\"run_at\":null,\"started_at\":null,\"state\":\"pending\",\"completed_at\":nu...s\":[],\"branch\":{\"id\":1,\"name\":\"dev\",\"repository\":{\"id\":321,\"url\":\"fakeurl.com\"}}}}"
(compared using ==)
# ./spec/serializers/testrun_spec.rb:11:in `block (2 levels) in <top (required)>'
Finished in 0.79448 seconds (files took 5.63 seconds to load)
1 example, 1 failure
Failed examples:
rspec ./spec/serializers/testrun_spec.rb:8 # TestrunSerializer creates special JSON for the API
Here is the RSpec test:
require 'rails_helper'
describe TestrunSerializer, type: :serializer do
let(:repo) { Repository.create(id: 321, url: "fakeurl.com") }
let(:branch) { Branch.create(id: 1,name: "dev", repository_id: repo.id) }
let(:testchunk) { Testchunk.create(id: 15, name: "flair") }
it "creates special JSON for the API" do
serializer = TestrunSerializer.new Testrun.new("id":1, name: "name", "run_at": nil, state: "pending", branch_id: branch.id)
testjob = Testjob.create(id: 8, active: false, testchunk_id: testchunk.id, testrun_id: 1)
expect(serializer.to_json).to eq('{"testrun":{"id":1,"run_at":null,"started_at":null,"state":"pending","completed_at":null,"testjobs":[{"id":2,"active":false,"testchunk_id":2,"testrun_id":1,"testchunk_name":"flair","testchunk":{"id":15,"name":"flair"}}],"branch":{"id":1,"name":"dev","repository":{"id":321,"url":"fakeurl.com"}}}}')
end
end
Here is the actual serializer:
class TestrunSerializer < ActiveModel::Serializer
attributes :id, :run_at, :started_at, :state, :completed_at, :testjobs
has_many :testjobs
has_one :branch
end
Technologies used: Rails 5.1, RSpec 3.6, Ruby 2.4
It looks like your testjobs are not matching
completed_at\":nu...r\"}}],\"branch\"
vs
completed_at\":nu...s\":[],
You should set up your specs so the testjobs are returned as well.
Please note that the diff string is cut in the middle - this is one of the most annoying part of eq matcher when used with strings.
Edit: You may wont to switch to comparing arrays/hashes instead of strings to get better diffs. expect(serializer).to eq {testrun: "..."} (Drop to_json in your assertions)
The reason why your test didn't pass is trivial: inside the it block, you assigned the Testrun id (1) while creating the Testjob record, but the Testrun record does not exist.
SomeActiveRecord.new() will not create any actual record until you invoke save() on it, or you can just invoke SomeActiveRecord.create for that.
some_active_record = SomeActiveRecord.new(...)
some_active_record.save
# or
some_active_record = SomeActiveRecord.create(...)
So the final solution may look something like:
it "creates special JSON for the API" do
testrun = Testrun.create(id: 1, name: "name", run_at: nil, state: "pending", branch_id: branch.id)
serializer = TestrunSerializer.new(testrun)
testjob = Testjob.create(id: 8, active: false, testchunk_id: testchunk.id, testrun_id: testrun.id)
expect(serializer.to_json).to eq('{"testrun":{"id":1,"run_at":null,"started_at":null,"state":"pending","completed_at":null,"testjobs":[{"id":2,"active":false,"testchunk_id":2,"testrun_id":1,"testchunk_name":"flair","testchunk":{"id":15,"name":"flair"}}],"branch":{"id":1,"name":"dev","repository":{"id":321,"url":"fakeurl.com"}}}}')
end
Improvement Scope:
Please have a look at the tests for :json adapter in the active_model_serializers repo: https://github.com/rails-api/active_model_serializers/blob/v0.10.6/test/action_controller/json/include_test.rb.
You can easily convert the tests to suite with rspec.
If you want to test the json output, then you should put the tests under controller or request specs; rather than in serializers. Because rendering json is the responsibility of the adapter; serializers merely feed the adapter with all the attributes and associations defined in them.
Working solution:
I added the line
serializer.testjobs << testjob
to explicitly associate the testjob with the object, and the test now passes.

Is there a way to seed hashes using the Faker gem?

I am trying to seed some data but I have a field that takes in a hash. When I do the following
50.times do
Event.create(
name: Faker::Internet.name,
data: Faker::Lorem.words(4),
uri: Faker::Internet.url
)
end
events = Event.all
I get an error saying that data is being seeded as an array since it's a hash field. Is there a work around this?
rake aborted!
Mongoid::Errors::InvalidValue:
Problem:
Value of type Array cannot be written to a field of type Hash
Summary:
Tried to set a value of type Array to a field of type Hash
I tried doing the following:
data: Faker::Lorem.words(4).to_h
but it doesn't seem to work.
You could do
Event.create(
name: Faker::Internet.name,
data: Hash[*Faker::Lorem.words(4)],
uri: Faker::Internet.url
)
Use Faker::Crypto.sha256.
Example:
> Faker::Crypto.sha256
=> "ee25a4b399dd3c748d310aedd458f0662d4736f470d2c8342553e1e8998b4bc2"

Mutate string for testing with rspec and factory_girl

I'd like to test the validation of a model's attribute with rspec and factory_girl. The 'special' thing is, that one attribute (the name) isn't allowed to start with Numbers or special signs like %,&,$, ...
For testing this it would be great to write only one example like
it "is invalid with name starting by special character" do
["1","$","%"].each do |i|
name = i + "test"
FactoryGirl.build(:tutu, name: name).should_not be_valid
end
end
This work's for the first case but it won't return the result for the whole list. Is it possible to tell rspec not to stop on the error?
Do this instead:
["1","$","%"].each do |i|
it "is invalid with name starting by '#{i}'" do
FactoryGirl.build(:tutu, name: "#{i}test").should_not be_valid
end
end

How can I write quoted values in en.yml?

I'm writing a script that will add new translations to the en.yml file. However, when I'm dumping them back to the file, my strings are in the following format:
some_key: This is the value
I'm trying to make the output be:
some_key: "This is the value"
I'm writing the translations like this:
File.open(yaml_file, "w") do |f|
f.write(translations.to_yaml)
end
Where translations is the hash containing all the translations.
Is there any way of adding these quotes, besides manually parsing/rewriting the YAML file?
The plan (unquotes) scalar representation is the preferred version when the scalar type doesn't require escaping.
In your case, the String:
This is the value
doesn't need to be in quotes, thus, if you supply the following YAML:
key: "This is the value"
the processor may return:
key: This is the value
because they are totally equivalent. However, if you actually want to enter a quoted string as value, then you should use:
key: '"This is the value"'
or escape the double quote:
key: "\"This is the value\""
I gave a quick look at the Psych emitter code, the one invoked by the to_yaml, and there doesn't seem to be an option to force quoting on scalar.
I don't even see the option implemented in the scalar emitter code.
def visit_Psych_Nodes_Scalar o
#handler.scalar o.value, o.anchor, o.tag, o.plain, o.quoted, o.style
end
In other words, you cannot enforce quoting.
Updated for hash conversion
def value_stringify(hash)
hash.each do |k,v|
if v.kind_of? Hash
hash[k]= value_stringify(v)
else
hash[k]="\"#{v}\""
end
end
end
Now use the converted hash to store yaml.
File.open(yaml_file, "w") do |f|
f.write(value_stringify(translations).to_yaml)
end
Now it should work..
The format you get is valid YAML. However, if you really want this you could temporarily modify your data before converting it.
Normal:
{ foo: "bar" }.to_yaml
# => foo: bar
With an space after:
{ foo: "bar " }.to_yaml
# => foo: 'bar '
Note that you get single quotes and not double quotes. So if you temporarily modifying your data you could add in an placeholder which you remove later.
Example:
{ foo: "foo --REMOVE-- ", bar: "bar --REMOVE-- " }.to_yaml
.gsub(' --REMOVE-- ', '')
# => foo: 'foo'
# bar: 'bar'

i18n sync of locals yaml keys

Similar question, but for java, Keeping i18n resources synced
How to keep the i18n yaml locals' keys in sync? i.e. when a key is added to en.yml, how to get those to nb.yml or ru.yml?
if I add the key my_label: "some text in english" next to my_title: "a title" I'd like to get this to my other locals I specify, as I can't do all translations and it should fall back to english in other languages
e.g en.yml
somegroup:
my_tile: "a title in english"
my_label: "some text in english"
othergroup:
...
I'd like go issue a command and get the whole key and value inject into the norwegian translation and the corresponding position, if missing. Then git diff would show all translations to do in this language.
nb.yml
somegroup:
my_tile: "En tittel på norsk"
+ my_label: "some text in english"
othergroup:
...
Are there any gems that do something like this? If you think it's a good idea, maybe I should take the time to make it myself. Other approaches?
Try the i18n_translation_spawner gem, it could be helpful.
I will check i18n_translation_spawner gem. In case that someone needs a not-so-fast but do the job, i use this script:
First we extend the Hash class in order to support deep_merge and to replace all their leaf values with some string.
require 'yaml'
class Hash
def deep_merge(hash)
target = dup
hash.keys.each do |key|
if hash[key].is_a? Hash and self[key].is_a? Hash
target[key] = target[key].deep_merge(hash[key])
next
end
target[key] = hash[key]
end
target
end
def fill_all_values value
each_key do |key|
if self[key].is_a?(String)
store(key,value)
else
self[key].fill_all_values value
end
end
end
end
Now we can use our merger of translations:
def merge_yaml_i18n_files(locale_code_A,locale_code_B,untranslated_message)
hash_A = YAML.load_file("i18n/#{locale_code_A}.yml")
hash_B = YAML.load_file("i18n/#{locale_code_B}.yml")
hash_A_ut = Marshal.load(Marshal.dump(hash_A))
hash_A_ut.fill_all_values(untranslated_message)
hash_B_ut = Marshal.load(Marshal.dump(hash_B))
hash_B_ut.fill_all_values(untranslated_message)
hash_A = hash_B_ut.deep_merge(hash_A)
hash_B = hash_A_ut.deep_merge(hash_B)
puts hash_A.to_yaml
puts hash_B.to_yaml
end
And finally, we call this method with:
merge_yaml_i18n_files('en','es','untranslated')
If we apply this function in the following i18n files:
es.yaml
test:
hello: Hola
only_es: abc
en.yaml
test:
hello: Hello
only_en: def
The result will be:
es.yaml
test:
hello: Hola
only_en: untranslated
only_es: abc
en.yaml
test:
hello: Hello
only_en: def
only_es: untranslated
You can use i18n-tasks gem for this.
It scans calls such as I18n.t('some.key') and provides reports on key usage, missing, and unused keys. It can also can pre-fill missing keys, including from Google Translate, and it can remove unused keys as well.

Resources