Testing with Rspec: Stubs, mocks, factories - what to choose?

Let's assume we have an ActiveModel model Article with fields :id and :author

class Article < ActiveRecord::Base

  def read
    puts 'I am reading'
  end

end

Terminology:

  • Stub - fake implementation of a method
  • Mock - fake implementation of an object
  • Double - a newer term to represent an object that stands in for another object

article.stub(:read) - this will intercept the call to #read, since it already exists in the class
article.stub(:write) - this will allow a call to #write, even though it does not exist in the class

The stub method is now deprecated, because it is a monkey patch of Object, but it can be used for a Rspec double.

article = double(Article) - will create an instance of a Rspec double class, which we can use to stand in for an instance of Article class.

The argument for double() may or may not exist, thus double('VirtualArticle') is valid.

There are several benefits:

  1. We do need to create an instance of a real Article class, which may be costly if its creation requires a lot of calculations or database interactions.
  2. We can use the stub method on the double to simulate required behaviour, e.g. article.stub(:write).and_return('I am writing').
  3. You can use the stub as a spy, to assert calls and arguments to stubbed out methods, e.g. expect(my_stub).to receive(:stubbbed_method).at_least(5).times.with({ arg_1 : 'val_1', arg_2: 'val_2 })
  4. And you can always call the original method implementation with allow(:my_stub).to receive(:stubbed_method).and_call_through

However, do not forget that a double does not know anything about the real class that it stands for, therefore be careful not to stub out may be any methods that do not exist in the real class.

The other problem may arise in a situation when you were stubbing out a real method, then that method got removed during refactoring, but your test would still be passing, because the double you are using does not actually know if that method exists or not.

This issue is dealt with 'verifying doubles' in Rspec 3.

Something for Rails

Terminology:

  • Mock_model - generates a test double that acts like an instance of ActiveModel
  • Stub_model - generates an instance of a real model class

article = mock_model(Article)

Mock model is useful when you need to pass the instance double somewhere where an Article object or an ActiveModel object is expected.

You would also get all the model fields accessible on a double, e.g. you can call article.author and article.id and those fields will have the values instantiated by the constructor of Article.

In this, case a mock model can only be created for an existing ActiveModel model class. If we query the class of article - we would get: class Article < ActiveRecord::Base

As with a normal double, you can stub out any methods on mock_model, as it does not know anything about the real implementation of the Article model class.

article = stub_model(Article)

Stub model is different in a way that it generates an instance of a real ActiveModel model.

This is useful when you need a double instance of an Article model, but with its existing methods in place, e.g. in this case article will have a real #read method.

In fact, this is just the same as doing article = Article.new. The only difference, that you can use the stub method on the stub_model.

FactoryGirl

Factory is something that can create multiple objects. Thus, a factory for Article would be some way of creating many article instances when we need them.

FactoryGirl does just that.

  • FactoryGirl.build(:article) - creates an instance of a real Article model, which is equivalent to Article.new
  • FactoryGirl.create(:article) - creates an instance and saves to to the database, which is equivalent to Article.create

This is useful, because FactoryGirl has a handful of methods for creating many instances with predefined field values and even associations.
Since these are instances of a real model, they will have all the instance variables and methods.

  • FactoryGirl.build_stubbed(:article) - is a shiny new method from FactoryGirl.

It creates non-persisted instance just as #build, but makes it look, as though it is persisted, by assigning an :id field.

It intercepts all the calls to the database and raises an error. This way you know that your test is accessing the database, when it probably should not.

If your factory contains associations, calling :build_stubbed would also call :build_stubbed when building associated models, unlike :build, which calls :create on associated models.


So, what to choose?

Ok, here is my personal vision of the case:

There are two main concerns when making a choice - that is speed and maintenance.

First of all, stub_model and mock_model are likely to be deprecated in Rspec 3. Therefore, use FactoryGirl to create an instance and Rspec's allow(article).to receive(:write).and_return to stub out any methods you need to. Alternatively, if you do not need to create many instances with different predefined fields, just use the regular Article.new or Article.create.

Secondly - obviously, saving an instance to the database is longer, than just instantiating it. Therefore, prefer to use FactoryGirl.build or build_stubbed over FactoryGirl.create, unless your tests rely on the instance to be saved in the database, because the database is queried for it, for example.

Third - use FactoryGirl.build_stubbed to control the access to the database.

And what about doubles? Well, doubles is certainly a choice for non-ActiveModel objects.

It is not such a great idea for ActiveModel model instances for reasons explained above.

However, in some cases you may find that instantiating an ActiveModel instance is too costly, because of its constructor inner workings, so a double with just needed method stubs can save you some testing time. Just do not forget to keep your doubles implementation with sync with the real objects.

As mentioned above, there is a concept of a 'verifying double' in Rspec3, that makes sure no non-existent methods are stubbed out, but you will need compare the speed of creating a verifying double with a real instance by FactoryGirl.build.

comments powered by Disqus