# Factory Bot With Rails

> At this stage of the project, we are introducing Factory Bot to help replace the default fixtures so we can test our PostsRepository code.

- Canonical: https://dennisokeeffe.com/blog/2022-02-24-factory-bot-with-rails
- Published: 2022-02-24
- Updated: 2022-02-24
- Tags: ruby, ruby-on-rails

This post will continue on the work that we have done for the last two posts by introducing [Factory Bot](https://github.com/thoughtbot/factory_bot) to help with writing our tests for the `Posts` repository.

Source code can be found [here](https://github.com/okeeffed/demo-repo-pattern-dry-validation)

## Prerequisites

1. [Part 1](https://blog.dennisokeeffe.com/blog/2022-02-22-repository-pattern-in-rails)
2. [Part 2](https://blog.dennisokeeffe.com/blog/2022-02-23-dry-validation-with-rails)

## Getting started

We will clone the project `demo-repo-pattern-dry-validation` and work from the branch `dry-validation-and-refactor`:

```s
# Clone the project
$ git https://github.com/okeeffed/demo-repo-pattern-dry-validation
$ cd demo-repo-pattern-dry-validation
$ git checkout dry-validation-and-refactor

# Install dependencies
$ bundler install factory_bot_rails --group "development,test"

# Create files required
$ mkdir -p spec/factories spec/repositories
$ touch spec/factories/post_factory.rb spec/repositories/posts_repository_spec.rb
```

Following on from the [Factory Bot Getting Started docs](https://github.com/thoughtbot/factory_bot/blob/master/GETTING_STARTED.md#rspec) for RSpec, we need to add the following to `spec/support/factory_bot.rb`:

```rb
RSpec.configure do |config|
  config.include FactoryBot::Syntax::Methods
end
```

Then in the `spec/rails_helper.rb` file, uncomment the line `Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f }`.

At this stage, our project is now ready to start creating our factory.

## Post factory

Inside of `spec/factories/post_factory.rb`, add the following:

```rb
FactoryBot.define do
  factory :post do
    title { "Title" }
    rating  { "Good" }
  end
end
```

Now that our factory is defined, we are ready to write some tests for our `PostsRespository` class.

## A quick refactor

Something came up between the last blog post and this one was that testing the `PostsRespository` class on its own will not include our `dry-validation` `PostContract` validator that we wrote in the last post.

The jury is still out on whether the decision is the right one, but I decided to refactor both our controller and repository classes so that the validation occurred in the repository class.

Ensure that you update your `app/repositories/posts_repository` to the following:

```rb
require 'dry/monads'
require 'post_contract'

Dry::Validation.load_extensions(:monads)

class PostsRepository
  class << self
    include Dry::Monads[:result]

    def create(title:, rating:)
      # Validate the params
      contract = PostContract.new
      validation = contract.call(title: title, rating: rating).to_monad
      return validation unless validation.success?

      post = Post.new(title: title, rating: rating)
      post.save!

      # `save!`` will raise if there is an error, so we assume here
      # that we were successful and return the post to follow RESTful conventions.
      # @see https://restfulapi.net/http-status-200-ok/
      Success(post)
    rescue ActiveRecord::ActiveRecordError => e
      Failure(e)
    rescue StandardError => e
      Failure(e)
    end

    def find_all
      posts = Post.all
      Success(posts)
    rescue ActiveRecord::ActiveRecordError => e
      Failure(e)
    rescue StandardError => e
      Failure(e)
    end

    def find_by_id(id:)
      post = Post.find_by!(id: id)
      Success(post)
    rescue ActiveRecord::RecordNotFound => e
      Failure(e)
    rescue ActiveRecord::ActiveRecordError => e
      Failure(e)
    rescue StandardError => e
      Failure(e)
    end
  end
end
```

Essentially we are adding the first three lines of the `create` function to now validate our parameters coming in.

Because of that, I have no refactored the file `app/controllers/posts_controller.rb` file to the following:

```rb
require 'dry/validation'
require 'dry/monads'
require 'posts_repository'

class PostsController < ApplicationController
  include Dry::Monads[:result, :do]

  def create
    case PostsRepository.create(title: params[:title], rating: params[:rating])
    in Success(post) then render json: post, status: :ok
    in Failure(ActiveRecord::ActiveRecordError) then internal_server_error(code: '002')
    in Failure(StandardError) then internal_server_error(code: '001')
    in Failure(Dry::Validation::Result => result) then unprocessable_entity(result: result)
    else internal_server_error(code: '003')
    end
  end

  def index
    case PostsRepository.find_all
    in Success(posts) then render json: posts, status: :ok
    in Failure(ActiveRecord::ActiveRecordError) then internal_server_error(code: '002')
    in Failure(StandardError) then internal_server_error(code: '001')
    else internal_server_error(code: '003')
    end
  end

  def show
    case PostsRepository.find_by_id(id: params[:id])
    in Success(post) then render json: post, status: :ok
    in Failure(ActiveRecord::RecordNotFound) then not_found
    in Failure(ActiveRecord::ActiveRecordError) then internal_server_error(code: '002')
    in Failure(StandardError) then internal_server_error(code: '001')
    else internal_server_error(code: '003')
    end
  end

  private

  def internal_server_error(code:)
    render json: { message: 'Internal server error', code: code }, status: :internal_server_error
  end

  def unprocessable_entity(result:)
    render json: { message: 'Unprocessable entity', errors: result.errors.to_h }, status: :unprocessable_entity
  end

  def not_found
    render json: { message: 'Not found' }, status: :not_found
  end
end
```

There changes I made here were to remove the private method `create_post` and just run the pattern matching on the `create` method directly on the result of `PostsRepository.create`.

With these changes in place, there were a couple of updates that I needed to make to the controller tests. From memory, I needed to update the tests that returned `422` to actually run against the validation without a mock. For reference sake, my `spec/controllers/posts_controller_spec.rb` file looks like the following:

```rb
require 'rails_helper'
require 'dry/monads'
# require 'dry/validation'
require 'post_contract'
require 'posts_repository'
require 'pry'

# Dry::Validation.load_extensions(:monads)

RSpec.describe PostsController, type: :controller do
  include Dry::Monads[:result]

  describe 'GET #index' do
    context 'successful responses' do
      it 'returns empty successful array when there are no posts' do
        stubbed_response = []
        posts_respository_klass = class_double(PostsRepository, find_all: Success([stubbed_response])).as_stubbed_const

        expect(posts_respository_klass).to receive(:find_all)

        get :index
        expect(response).to have_http_status(:success)
        expect(response.parsed_body).to eq(JSON.parse(stubbed_response.to_json))
      end

      it 'returns correct array when there are posts' do
        posts = [{
          id: 1,
          title: 'Cool post',
          rating: 'Good'
        }]
        posts_respository_klass = class_double(PostsRepository, find_all: Success([posts])).as_stubbed_const

        expect(posts_respository_klass).to receive(:find_all)

        get :index
        expect(response).to have_http_status(:success)
        expect(response.parsed_body).to eq(JSON.parse(posts.to_json))
      end
    end

    context 'unsuccessful responses' do
      it 'returns 500 when there is an ActiveRecord::ActiveRecordError' do
        res = { message: 'Internal server error', code: '002' }
        posts_respository_klass = class_double(PostsRepository,
                                               find_all: Failure(ActiveRecord::ActiveRecordError.new)).as_stubbed_const

        expect(posts_respository_klass).to receive(:find_all)

        get :index
        expect(response).to have_http_status(:internal_server_error)
        expect(response.parsed_body).to eq(JSON.parse(res.to_json))
      end

      it 'returns 500 when there is a StandardError' do
        res = { message: 'Internal server error', code: '001' }
        posts_respository_klass = class_double(PostsRepository, find_all: Failure(StandardError.new)).as_stubbed_const

        expect(posts_respository_klass).to receive(:find_all)

        get :index
        expect(response).to have_http_status(:internal_server_error)
        expect(response.parsed_body).to eq(JSON.parse(res.to_json))
      end
    end
  end

  describe 'GET #show' do
    context 'successful responses' do
      it 'returns correct array when there is a post with the expected ID' do
        stubbed_response = {
          id: 1,
          title: 'Cool post',
          rating: 'Good'
        }
        posts_respository_klass = class_double(PostsRepository, find_by_id: Success(stubbed_response)).as_stubbed_const

        expect(posts_respository_klass).to receive(:find_by_id).with(id: '1')

        get :show, params: { id: '1' }
        expect(response).to have_http_status(:success)
        expect(response.parsed_body).to eq(JSON.parse(stubbed_response.to_json))
      end
    end

    context 'unsuccessful responses' do
      it 'returns 500 when there is an ActiveRecord::ActiveRecordError' do
        res = { message: 'Internal server error', code: '002' }
        posts_respository_klass = class_double(PostsRepository,
                                               find_by_id: Failure(ActiveRecord::ActiveRecordError.new)).as_stubbed_const

        expect(posts_respository_klass).to receive(:find_by_id).with(id: '1')

        get :show, params: { id: '1' }
        expect(response).to have_http_status(:internal_server_error)
        expect(response.parsed_body).to eq(JSON.parse(res.to_json))
      end

      it 'returns 500 when there is a StandardError' do
        res = { message: 'Internal server error', code: '001' }
        posts_respository_klass = class_double(PostsRepository, find_by_id: Failure(StandardError.new)).as_stubbed_const

        expect(posts_respository_klass).to receive(:find_by_id).with(id: '1')

        get :show, params: { id: '1' }
        expect(response).to have_http_status(:internal_server_error)
        expect(response.parsed_body).to eq(JSON.parse(res.to_json))
      end

      it 'returns 404 when there is no matching post' do
        res = { message: 'Not found' }
        posts_respository_klass = class_double(PostsRepository).as_stubbed_const
        allow(posts_respository_klass).to receive(:find_by_id).and_return(Failure(ActiveRecord::RecordNotFound.new))

        expect(posts_respository_klass).to receive(:find_by_id).with(id: '1')

        get :show, params: { id: '1' }
        expect(response).to have_http_status(:not_found)
        expect(response.parsed_body).to eq(JSON.parse(res.to_json))
      end
    end
  end

  describe 'POST #create' do
    context 'successful responses' do
      it 'returns 200 and created post when the post is valid' do
        stubbed_response = { id: 1, title: 'Title', rating: 'Good' }
        posts_respository_klass = class_double(PostsRepository, create: Success(stubbed_response)).as_stubbed_const

        expect(posts_respository_klass).to receive(:create).with(title: 'Title', rating: 'Good')

        post :create, params: { title: 'Title', rating: 'Good' }, as: :json
        expect(response).to have_http_status(:ok)
        expect(response.parsed_body).to eq(JSON.parse(stubbed_response.to_json))
      end
    end

    context 'invalid params' do
      it 'returns 422 when there are invalid params for title' do
        res = { message: 'Unprocessable entity', errors: { title: ['must be a string'] } }

        post :create, params: { title: 100, rating: 'Good' }, as: :json
        expect(response).to have_http_status(:unprocessable_entity)
        expect(response.parsed_body).to eq(JSON.parse(res.to_json))
      end

      it 'returns 422 when there are invalid params for rating' do
        res = { message: 'Unprocessable entity', errors: { rating: ['must be a string'] } }

        post :create, params: { title: 'Title', rating: 100 }, as: :json
        expect(response).to have_http_status(:unprocessable_entity)
        expect(response.parsed_body).to eq(JSON.parse(res.to_json))
      end
    end

    context 'unsuccessful responses' do
      it 'returns 500 when there is an ActiveRecord::ActiveRecordError' do
        res = { message: 'Internal server error', code: '002' }
        posts_respository_klass = class_double(PostsRepository,
                                               create: Failure(ActiveRecord::ActiveRecordError.new)).as_stubbed_const

        expect(posts_respository_klass).to receive(:create).with(title: 'Title', rating: 'Good')

        post :create, params: { title: 'Title', rating: 'Good' }, as: :json
        expect(response).to have_http_status(:internal_server_error)
        expect(response.parsed_body).to eq(JSON.parse(res.to_json))
      end

      it 'returns 500 when there is a StandardError' do
        res = { message: 'Internal server error', code: '001' }
        posts_respository_klass = class_double(PostsRepository, create: Failure(StandardError.new)).as_stubbed_const

        expect(posts_respository_klass).to receive(:create).with(title: 'Title', rating: 'Good')

        post :create, params: { title: 'Title', rating: 'Good' }, as: :json
        expect(response).to have_http_status(:internal_server_error)
        expect(response.parsed_body).to eq(JSON.parse(res.to_json))
      end

      it 'returns 500 when there is an unexpected state' do
        res = { message: 'Internal server error', code: '003' }
        posts_respository_klass = class_double(PostsRepository, create: Failure(Exception.new)).as_stubbed_const

        expect(posts_respository_klass).to receive(:create).with(title: 'Title', rating: 'Good')

        post :create, params: { title: 'Title', rating: 'Good' }, as: :json
        expect(response).to have_http_status(:internal_server_error)
        expect(response.parsed_body).to eq(JSON.parse(res.to_json))
      end
    end
  end
end
```

Now we can get on to writing our repository tests!

## The Posts Repository Spec

Thanks to our post factory, we can use some of the Factory Bot helper methods to test our repository.

With Factory Bot, we have two methods to create a post:

1. `build(:post)` where the model is not saved.
2. `create(:post)` where the model is saved.

With the tests that I am writing, I will essentially be using `build` to create an object quickly to test for our `PostsRepository#create` method while the Factory Bot `create` helper I will use for the tests that require fetching a value from the database.

In the `spec/repositories/posts_repository_spec.rb` file, add the following:

```rb
require 'rails_helper'
require 'posts_repository'

RSpec.describe PostsRepository do
  describe '#create' do
    context 'valid parameters' do
      let(:subject) { build(:post) }

      it 'returns a post' do
        result = PostsRepository.create(title: subject.title, rating: subject.rating)
        expect(result.success?).to eq(true)

        post = result.value!
        expect(post).to be_a(Post)
        expect(post.title).to eq(subject.title)
        expect(post.rating).to eq(subject.rating)
      end
    end

    context 'invalid parameters' do
      let(:invalid_title) { build(:post, title: nil) }
			let(:invalid_rating) { build(:post, rating: nil) }

      it 'returns a validation error when the title is invalid' do
        result = PostsRepository.create(title: invalid_title.title, rating: invalid_title.rating)
        expect(result.failure?).to eq(true)
        expect(result.failure).to be_a(Dry::Validation::Result)
      end

			it 'returns a validation error when the rating is invalid' do
        result = PostsRepository.create(title: invalid_rating.title, rating: invalid_rating.rating)
        expect(result.failure?).to eq(true)
        expect(result.failure).to be_a(Dry::Validation::Result)
      end
    end
  end
end
```

The above tests that I have written test the `#create` method of the `PostsRepository` class for both valid and invalid parameters.

- In the valid parameter test, I simply use `build(:post)` to create our default object to test against.
- With the invalid parameter tests, I pass extra invalid parameters to the `#create` method and check for a `Failure` monad of type `Dry::Validation::Result`.

You could go into more detail here and check the different varieties of invalid parameters, but I will leave that for another time.

As for the rest of the tests, I checked the error routes by stubbing out methods on `Post` and `PostContract`:

The final tests are follows:

```rb
require 'rails_helper'
require 'posts_repository'

RSpec.describe PostsRepository do
  describe '#create' do
    context 'valid parameters' do
      let(:subject) { build(:post) }

      it 'returns a post' do
        result = PostsRepository.create(title: subject.title, rating: subject.rating)
        expect(result.success?).to eq(true)

        post = result.value!
        expect(post).to be_a(Post)
        expect(post.title).to eq(subject.title)
        expect(post.rating).to eq(subject.rating)
      end
    end

    context 'invalid parameters' do
      let(:invalid_title) { build(:post, title: nil) }
      let(:invalid_rating) { build(:post, rating: nil) }

      it 'returns a validation error when the title is invalid' do
        result = PostsRepository.create(title: invalid_title.title, rating: invalid_title.rating)
        expect(result.failure?).to eq(true)
        expect(result.failure).to be_a(Dry::Validation::Result)
      end

      it 'returns a validation error when the rating is invalid' do
        result = PostsRepository.create(title: invalid_rating.title, rating: invalid_rating.rating)
        expect(result.failure?).to eq(true)
        expect(result.failure).to be_a(Dry::Validation::Result)
      end
    end

    context 'error responses' do
      let(:subject) { build(:post) }

      it 'should return a Failure monad for an ActiveRecordError' do
        PostContract.stub(:new).and_raise(ActiveRecord::ActiveRecordError)

        result = PostsRepository.create(title: subject.title, rating: subject.rating)
        expect(result.failure?).to eq(true)
        expect(result.failure).to be_a(ActiveRecord::ActiveRecordError)
      end

      it 'should return a Failure monad for a StandardError' do
        PostContract.stub(:new).and_raise(StandardError)

        result = PostsRepository.create(title: subject.title, rating: subject.rating)
        expect(result.failure?).to eq(true)
        expect(result.failure).to be_a(StandardError)
      end
    end
  end

  describe '#find_all' do
    context 'successful responses' do
      let(:post_one) { create(:post) }
      let(:post_two) { create(:post) }

      it 'should return as a valid array of posts' do
        result = PostsRepository.find_all
        expect(result.success?).to eq(true)

        posts = result.value!
        expect(posts).to eq([post_one, post_two])
      end
    end

    context 'error responses' do
      it 'should return a Failure monad for an ActiveRecordError' do
        Post.stub(:all).and_raise(ActiveRecord::ActiveRecordError.new)

        result = PostsRepository.find_all
        expect(result.failure?).to eq(true)
        expect(result.failure).to be_a(ActiveRecord::ActiveRecordError)
      end

      it 'should return a Failure monad for a StandardError' do
        Post.stub(:all).and_raise(StandardError.new)

        result = PostsRepository.find_all
        expect(result.failure?).to eq(true)
        expect(result.failure).to be_a(StandardError)
      end
    end
  end

  describe '#find_by_id' do
    let(:subject) { create(:post) }

    context 'valid parameters' do
      it 'returns a valid post' do
        result = PostsRepository.find_by_id(id: subject.id)
        expect(result.success?).to eq(true)

        post = result.value!
        expect(post).to be_a(Post)
        expect(post.title).to eq(subject.title)
        expect(post.rating).to eq(subject.rating)
      end
    end

    context 'error responses' do
      it 'should return a Failure monad for an RecordNotFound' do
        result = PostsRepository.find_by_id(id: nil)
        expect(result.failure?).to eq(true)
        expect(result.failure).to be_a(ActiveRecord::ActiveRecordError)
      end

      it 'should return a Failure monad for an ActiveRecordError' do
        Post.stub(:find_by!).and_raise(ActiveRecord::ActiveRecordError)

        result = PostsRepository.find_by_id(id: subject.id)
        expect(result.failure?).to eq(true)
        expect(result.failure).to be_a(ActiveRecord::ActiveRecordError)
      end

      it 'should return a Failure monad for a StandardError' do
        Post.stub(:find_by!).and_raise(StandardError)

        result = PostsRepository.find_by_id(id: subject.id)
        expect(result.failure?).to eq(true)
        expect(result.failure).to be_a(StandardError)
      end
    end
  end
end
```

If you want the validate the difference between `build` and `create`, you can swap out `create` for `build` on the helper methods testing the `find_by_id` method and you'll notice that they will fail because the subject model was not saved prior to the test.

## Summary

Today's post demonstrated how to use Factory Bot when testing classes that rely on database models.

The factories allow a quick way to build out valid models to use for testing and the `create` helper is a great tool for writing tests that check against finding particular values.

This essentially concludes the little three-part series that I have been working on that explored `FactoryBot`, `dry-validation` and the repository pattern.

Moving on, I believe the next topics I have prepared to write on are Sorbet for type checking!

## Resources and further reading

- [Rails getting started](https://guides.rubyonrails.org/getting_started.html)
- [Factory Bot](https://github.com/thoughtbot/factory_bot)
- [Source code](https://github.com/okeeffed/demo-repo-pattern-dry-validation)

_Photo credit: [`nickhh`](https://unsplash.com/@nickhh)_
