Faker RetryLimitExceeded and I18n

The problem

At some point our specs started to randomly fall with the following exception:

Faker::UniqueGenerator::RetryLimitExceeded:
       Retry limit exceeded for country_code

Configuration

The problematic field was defined in the Country factory, which was used only in a few specs:

# spec/factories/country.rb

FactoryBot.define do
  factory :country do
    code { Faker::Address.unique.country_code }
    name { Faker::Address.unique.country }
  end
end

Also to avoid Faker::UniqueGenerator::RetryLimitExceeded exception we clear the record of unique values between tests:

# spec/rails_helper.rb

RSpec.configure do |config|
  # ...
  config.before(:all) do
    Faker::UniqueGenerator.clear
  end
  # ...
end

Together with 250 country codes defined in Faker it should guarantee that Faker::UniqueGenerator::RetryLimitExceeded won’t be raised. It should :)

The solution

After debugging it turned out that the problem was in a completly different place. One of the remaining specs was changing I18n.locale settings to :pl.

context 'when changing locale' do
  before { I18n.locale = :pl }

  it 'sets correct formatting' do
    expect(Money.from_amount(123_456.78, 'USD').format).to eq('$123 456,78')
  end
end

And that was the mistake because I18n.locale changes the global state. As a result, all the remaining specs executed after that one were using Polish locale.

But how that can generate Faker::UniqueGenerator::RetryLimitExceeded exception?

As the locale has changed to :pl, Faker started to use Polish locale, and in the Polish translation, country_code contains only one code: PL. When two countries were created in one context, using Country factory, Faker was raising an exception because there was only one country code to use. Together with RSpec configured to run specs in random order, it was generating random false negatives.

The specs were updated not to change the locale, but execute the code in selected locale scope.

 context 'when changing locale' do
  let(:locale) { :pl }

  it 'sets correct formatting' do
    I18n.with_locale(locale) do
      expect(Money.from_amount(123_456.78, 'USD').format).to eq('$123 456,78')
    end
  end
end

Summary

I18n.locale chagnes the global state which is not reset between the specs. The correct way to write the test and avoid the side effects is to use I18n.with_locale to execute the code in selected locale scope.