1

I am building a Rails App (I am new to this, so forgive me if some of the wording is clumsy). I am trying to write tests (with RSpec) which draw and use data from the database, and I am having trouble writing the tests in a concise way.

Some of the tests (such as signing up a user, or creating content) seem to be best suited to having a fresh database, while some require a database populated fixtures.

At the moment I am using the database cleaner gem, with the following configuration:

  config.before(:suite) do
    DatabaseCleaner.clean_with(:truncation)
    DatabaseCleaner.strategy = :truncation   end

  # start the transaction strategy as examples are run    
   config.around(:each) do |example|
     DatabaseCleaner.cleaning do
       example.run
     end    
   end

The reason I am using truncation for both strategies is that I prefer to have the id values completely refreshed between examples (so that if I create in one test, and then create in a second test, the second example should have id 1 rather than 2). I am not sure what the various strategies mean exactly - I have found this question which appears to explain them in terms of SQL syntax, but I'm not very familiar with that so my understanding is still quite vague. I believe the database is managed with PostgreSQL, but I rarely have to interact with it directly through that so I'm not particularly experienced.

So my database is completely dropped and constructed from scratch between every example - if I want a clean database then this is ideal, but if I want to simply load the fixtures then it can take a while to create all of the models. It feels like I should be able to have a 'cached' version of the fixtures, which I can load for those examples it's appropriate for. But I have no idea how to do this, if it is even possible. Is there a way?

Edit: Following a discussion in the comments, I suspect that I may want to remove Database Cleaner and use default Rails fixtures instead. I have tried this, and the only problem I'm having with it is the same as I had with the transaction strategy described above. That is: when test-created records are rolled back, the id is not rolled back, and this is awkward behaviour. If I create a user for the purpose of running a test, it is convenient to refer to it just as User.find(1), which is impossible if the id does not reset.

It might be that this is some kind of red flag, and I shouldn't be doing it (I am open to doing something else). I realise too that I could just say User.first to get the same behaviour, and this might be better. I'm not sure what's appropriate.

preferred_anon
  • 538
  • 4
  • 11

1 Answers1

1

DatabaseCleaner is not intended to be used with fixtures. Its intended to be used with factories. ActiveRecord::Fixtures has its own rollback mechanism.

There is a really big conceptional difference.

Fixtures are like this huge set of static dummy data that gets thrown into the database for each example and then gets reset with a transaction. The big con of fixtures is that the more fixtures you have the more complex the initial state of the application is and it encourages a tight coupling between the tests and the fixtures themselves.

This is an example which shows how the value "Marko Anastasov" magically appears from somewhere outside the code:

RSpec.describe User do
  fixtures :all

  describe "#full_name" do
    it "is composed of first and last name" do
      user = users(:marko)
      expect(user.full_name).to eql "Marko Anastasov"
    end
  end
end

Although fixtures have hade a resurgence lately due to perceived simplicity (along with Minitest).

Factories are object factories that produce unique records. Instead of having a bunch of junk floating around you start each example with a blank state and then use the factories to populate the database with the exact state to replicate the scenario you are testing. Done right this minimizes test ordering issues, flapping tests and changing fixtures breaking tests.

RSpec.describe User do
  describe "#full_name" do
    it "is composed of first and last name" do
      user = FactoryBot.create(:user)
      expect(user.full_name).to eql "#{user.first_name} #{user.last_name}"
    end
  end
end

This is an example of a good factory that would generate psuedorandom values:

require 'ffaker'

FactoryBot.define do
  factory :user do
    first_name { FFaker::Name.first_name }
    last_name { FFaker::Name.last_name }
  end
end
max
  • 96,212
  • 14
  • 104
  • 165
  • Thanks for the answer! Indeed, fixtures and factories are different things, and I happen to be using each when I think they're appropriate. Some tests (like creating content from scratch) feel more 'factory-like', and some tests feel more 'fixture-like' (where the behaviour I want to test feels like it depends on the contents of the db). I'll have a read through the page you linked and if I still feel unclear about what to do I'll come back here. – preferred_anon Sep 03 '18 at 15:51
  • I really wouldn't mix them - you can use both to achieve the same goals but using both in the same project won't work well. You´re actually just removing the only benefit of fixtures which is simplicity / speed. Especially since using DatabaseCleaner is not compatible with ActiveRecord::Fixtures and will make a mess. – max Sep 03 '18 at 15:54
  • I see. I think I prefer to use fixtures then, as the majority of the behaviour I'm interested in testing feels like it relies on the database structure in a key way that makes factory-based tests hard to write. But in some places where the behaviour is more simple, it feels better to use factories, since that way I know that no special features of the fixtures are making the tests pass 'by coincidence'. I'll have a go at removing DBCleaner from my project and see if the rollback works the way I want. – preferred_anon Sep 03 '18 at 16:17
  • By the way, what is it that makes them 'not compatible'? – preferred_anon Sep 03 '18 at 16:17
  • I've added some detail to the question (to avoid cluttering the comments) – preferred_anon Sep 03 '18 at 16:46
  • 1
    Fundamential differences in how they work and the fact that you either want one of them the handle the reset between tests (or DatabaseCleaner will nuke your fixtures). This means that you have to spread out the test config in your examples depending on which you are using. Its probably possible but its just overcomplicated and not really a good idea. I would liken this to using both MySQL and Postgres for the same project. You can - but WHY??! – max Sep 03 '18 at 16:51
  • 1
    Also hardcoding ID's is not a good idea no matter if you are using fixtures or factories. You should be more concerned with avoiding test ordering issues which are really a pain to deal with than lazy convenience. If you´re hardcoding IDs you´re hardcoding not just the fixture data but also the order that they are inserted. This is bound to break. – max Sep 03 '18 at 16:53
  • I've removed all dependency on explicit IDs (I think I had misunderstood what they were really for). I've basically removed all dependence on DBCleaner, and I've set up two different testing areas for 'unit' vs. 'integration' type tests. Thanks for all the clarification @max. – preferred_anon Sep 05 '18 at 09:21