RottenSoftware

Random thoughs about software development

Hanami - Entities and Repositories

In one of the previous posts we have created a list of words. This list has one big flaw, though - it’s hard-coded. We would like to display dynamic data there. To achieve this we will introduce two parts of Hanami ecosystem responsible for dealing with data - entities and repositories.

What are Entities?

Entity is the core of the application. This is where business logic lives. They are defined by their identity. They are decoupled from the persistence layer - database - which makes them lightweight and easy to test. Every entity has a schema associated with it. It can be either schema derived automatically from the database table definition (if we are using SQL database), or custom schema defined inside entity class. If we are not using SQL database we are obliged to define a custom schema.

The role of the schema is to:

  • whitelist the data used during initialization
  • enforce data integrity

When we are defining custom schemas Hanami provides data types, which are based on the dry-types gem.

What are Repositories

Repository is an object that lies between entity and the persistence layer. It provides an API to query and execute commands on the database. It’s storage independent and uses specific adapters that implement low-level code that deals with the database.

Repository is quite a common pattern in software development world. It provides many advantages:

  • applications depend on a stable API
  • many data sources can live inside one application
  • data sources can be changed easily

By design, all query methods in repositories are private. The goal is to create intention revealing APIs. So, instead of creating this method:

SomeRepository.new.where(reference_id: 123).order(:created_at).limit(2)

this method is much preferred:

def fetch_recent_records(reference, limit: 2)
  objects.where(reference_id: reference.id).order(:created_at).limit(2)
end

Create first model

Entities and repositories are tightly connected with each other, so Hanami provides a generator to create both at one go:

bundle exec hanami generate model word

This will generate following output:

      create  lib/shyshka/entities/word.rb
      create  lib/shyshka/repositories/word_repository.rb
      create  spec/shyshka/entities/word_spec.rb
      create  spec/shyshka/repositories/word_repository_spec.rb

Notice that files are placed in the lib/project_name folder, not to the apps/web folder. This is because they define our domain model and this will be shared across multiple application that we could create under one Hanami project.

Generate migration

Our next task is to alter the database structure, so it will be able to store word records. Hanami way of managing database schema is migration. So, in order to prepare database we need to use another generator:

bundle exec hanami generate migration add_words

This will generate migration file and store it inside db/migrations folder.

Name of the migration file is prefixed with timestamp.

Change migration file so it will look like this:


Hanami::Model.migration do
  change do
    create_table :words do
      primary_key :id

      column :name, String, null: false
      column :translation, String, null: false

      column :created_at, DateTime, null: false
      column :updated_at, DateTime, null: false
    end
  end
end

We’ve created table named words with primary key id, string columns name and translation, and two columns storing info about creation and update time of the record.

To run this migration type:

bundle exec hanami db prepare

How to use entities and repositories?

Since we are using SQL database - our schema will be derived from the db structure, so there is no need to change lib/shyshka/entities/word.rb file. Let’s write a test which will confirm that we can initialize our entity with expected arguments. Fill spec/shyshka/entities/word_spec.rb with this code:

require 'spec_helper'

describe Word do
  it 'can be initialized with attributes' do
    word = Word.new(name: 'pies', translation: 'dog')
    word.name.must_equal 'pies'
    word.translation.must_equal 'dog'
  end
end

When we run our test suite we will see the following error:

PG::UndefinedTable: ERROR:  relation "words" does not exist (Sequel::DatabaseError)

This is because we migrated only our development database and not the one that is used during tests. To fix this type this command:

HANAMI_ENV=test bundle exec hanami db prepare

As you see, HANAMI_ENV env variable defines which environment should be used. By default it’s set to development.

After running tests now with the command:

bundle exec rake test

we should see, that all the tests are passing.

Now we can test our repository as well. To do so we need to access the hanami console. We can do this with this command:

bundle exec hanami console

We can initialize repository by typing:

repository = WordRepository.new

We can check if we have any words stored in the db:

repository.all
=> []

To create a new word you type:

word = repository.create(name: "kot", translation: "cat")
=>  => #<Word:0x007ffdb6369c58 @attributes={:id=>1, :name=>"kot", :translation=>"cat", :created_at=>2017-04-01 10:36:28 UTC, :updated_at=>2017-04-01 10:36:28 UTC}>

We can verify that the word has been stored by typing:

repository.all
=> [#<Word:0x007ffdb6351a40 @attributes={:id=>1, :name=>"kot", :translation=>"cat", :created_at=>2017-04-01 10:36:28 UTC, :updated_at=>2017-04-01 10:36:28 UTC}>]

or:

repository.find(word.id)
 => #<Word:0x007ffdb6329860 @attributes={:id=>1, :name=>"kot", :translation=>"cat", :created_at=>2017-04-01 10:36:28 UTC, :updated_at=>2017-04-01 10:36:28 UTC}>

You can find more information about repository API here.

Displaying data from the database

Now we have tools to deal with the database, so it’s the highest time to get rid of the hard-coded data from our list of words.

Let’s start with the feature spec we’ve created to test list of words. Open spec/web/features/list_words_spec.rb and let’s use repository to add some words to the test database there:

require 'features_helper'

describe 'List words' do
  let(:repository) { WordRepository.new }
  before do
    repository.clear

    repository.create(name: 'pies', translation: 'dog')
    repository.create(name: 'kot', translation: 'cat')
  end  

  it 'displays list of the words' do
    visit '/'

    within '#words' do
      assert page.has_css?('.word', count: 2), 'Expected to find 2 words'
    end
  end
end

Run test and make sure that all the tests are passing.

Next step is to change the view as well. We will stop here for a second to think what we actually want to achieve. We want to handle 2 scenarios:

  • list all the words we have in our database
  • display message that we don’t have any words yet if we just started using our system

We also want to make sure that our template will have all the data accessible that it needs to render the view. In our case it’s the list of words.

Following the philosophy of BDD let’s add some tests first. Change spec/web/controllers/words/index_spec.rb to look like this:

require 'spec_helper'
require_relative '../../../../apps/web/views/words/index'

describe Web::Views::Words::Index do
  let(:exposures) { Hash[words: []] }
  let(:template)  { Hanami::View::Template.new('apps/web/templates/words/index.html.erb') }
  let(:view)      { Web::Views::Words::Index.new(template, exposures) }
  let(:rendered)  { view.render }

  it 'exposes #words' do
    view.words.must_equal exposures.fetch(:words)
  end

  describe 'when there are no words' do
    it 'shows a placeholder message' do
      rendered.must_include('<p class="placeholder">There are no words yet.</p>')
    end
  end

  describe 'when there are words' do
    let(:word1)     { Word.new(name: 'pies', translation: 'dog') }
    let(:word2)     { Word.new(name: 'kot', translation: 'cat') }
    let(:exposures) { Hash[words: [word1, word2]] }

    it 'lists them all' do
      rendered.must_include('pies')
      rendered.must_include('kot')
    end

    it 'hides the placeholder message' do
      rendered.wont_include('<p class="placeholder">There are no words yet.</p>')
    end
  end  
end

Our test should fail now because we still have the words hard-coded in the template.

Let’s fix it now.

<h1>All words</h1>

<% if words.any? %>
  <div id="words">
    <% words.each do |word| %>
      <div class="word row">
        <div class="col-xs-6"><%= word.name %></div>
        <div class="col-xs-6"><%= word.translation %></div>
      </div>
    <% end %>
  </div>
<% else %>
  <p class="placeholder">There are no words yet.</p>
<% end %>

When we run our tests now we will see that it still fails.

NameError: undefined local variable or method `words' for #<Hanami::View::Rendering::Scope:0x007fe6b71cef30>

It complains that it does not know what words are. This is expected since we did not expose the variable from the controller, where it mend to be created to the view. To fix this make your apps/web/contollers/words/index.rb file look like this:

module Web::Controllers::Words
  class Index
    include Web::Action

    expose :words

    def call(params)
      @words = WordRepository.new.all
    end
  end
end

After this change we should see that all the tests are passing.

How can we test controllers?

Last thing is making sure that our controller will expose the variables we expect. Let’s add out first controller test then.

Controllers are supposed to be a thin layer that gets the request params, calls repositories to fetch the necessary data and then exposes those data to the view so it can handle rendering properly. So to test controller sufficiently all we need to do is to make sure it returns correct status code and exposes correct variables. Our spec/web/controllers/words/index_spec.rb file should look like this:

require 'spec_helper'
require_relative '../../../../apps/web/controllers/words/index'

describe Web::Controllers::Words::Index do
  let(:action) { Web::Controllers::Words::Index.new }
  let(:params) { Hash[] }
  let(:repository) { WordRepository.new }

  before do
    repository.clear

    @word = repository.create(name: 'pies', translation: 'dog')
  end

  it 'is successful' do
    response = action.call(params)
    response[0].must_equal 200
  end

  it 'exposes all words' do
    action.call(params)
    action.exposures[:words].must_equal [@word]
  end
end

Now all our tests should pass.

Summary

We achieved quite a lot here:

  • we added some dynamic data to our list of words through entities and repositories
  • we learned how to test views
  • what is the purpose of the controller and how to test it

Code for this project is open sourced and can be found here

Our next task will be adding a form to create new words, so stay tuned!

Written on April 1, 2017
>