Thought Stream Rails App Tutorial Part 2

In part 2, we will write the methods for color coding each "thought" (seperated by commas) of our thought streams based on frequency in the database. We will adhere to industry best practices by following TDD (test driven development). Furthermore, we will practice "red, green, refactor" and will use RSpec as a testing framework. We will write tests that come up red (fail), then write the methods that our tests test so that they pass the tests (green), then we will refactor if we can.

Setting up RSpec

Head to rubygems.org, and search for "rspec-rails". Select "rspec-rails" and it should have at least 111 million downloads. On the right copy the GEMFILE to clipboard. Go to Gemfile and paste it in the development, test group.

Also on rubygems.org, search for "factory bot rails". Select "factory_bot_rails" and it should have at least 52 million downloads. On the right copy the GEMFILE to clipboard.

In my Gemfile I am adding:


gem 'rexml', '~> 3.2', '>= 3.2.4'
gem 'timecop', '~> 0.9.4'  #Should this be here?
gem 'rails-controller-testing'

group :development, :test do
  # Call 'byebug' anywhere in the code to stop execution and get a debugger console
  # gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
  gem 'rspec-rails', '~> 5.0', '>= 5.0.1'
  gem 'factory_bot_rails', '~> 6.1'
  gem 'pry', '~> 0.13.1'
  gem 'pry-byebug', '~> 3.9'
end

Run

$ bundle install

Run

$ rails generate rspec:install

We are ready to write some tests, but first read up on file structure with RSpec. If you use Minitest, it comes with an already established file directory tree, but RSpec is a build your own kind of thing.

In the spec directory, create a new directory called "helpers". We wish to test the helper methods that we will write in app/helpers/entries_helper.rb. You can read up about writing helper tests here.

In spec/helpers create a new file called "entries_helper_spec.rb".

Instead of manually creating the helpers directory and the spec ruby file, you could have run $ rails g rspec:helpers entries_helper

In spec create a "factories" directory. In factories create a file called "entry.rb". Now add the following to

FactoryBot.define do
  factory :entry do
  end
end

In spec create a "controllers" directory and inside that directory create a file called "entries_controller_spec.rb". If you want a fast way to create new folders and files and you are using VSCode, install "advanced-new-file" by patbenatar. It lets' you set a shortcut like cmd+n then fuzzy match auto complete the directory you want to create the file in, then type the file name, all without using the track pad or mouse.

Now in spec/controllers/entries_controller_spec.rb let's test the controller.

require "rails_helper"

RSpec.describe EntriesController, :type => :controller do
  describe "create" do
    subject{post :create, :params => {:entry => {:thoughts => thoughts}}}
    let(:thoughts){"PIZZA"}

    it "assigns the variable" do
      subject
      entry = Entry.all.last
      expect(assigns(:entry)).to eq(entry)

      expect(entry).to have_attributes(:thoughts => thoughts)
    end
  end
end

In spec create a "helpers" directory and inside that directory create a file called "entries_helper_spec.rb".

Now in spec/helpers/entries_helpers_spec.rb let's test the test the helper methods that we are about to write. This is a practice of test driven development TDD. We will be writing the following methods: get_red, and create_prior_word_count_hash.

require "rails_helper"

RSpec.describe EntriesHelper, :type => :helper do
  describe "#get_red" do
    let!(:entry1){FactoryBot.create(:entry, thoughts: 'taxes, walk, study')}
    let!(:entry2){FactoryBot.create(:entry, thoughts: 'taxes, walk')}
    let!(:entry3){FactoryBot.create(:entry, thoughts: 'taxes')}
    let(:entries){Entry.all}

    it "returns 20 times the number of prior occurences of the string passed in" do
      # binding.pry
      expect(helper.get_red(entries, "taxes", entry3.created_at.to_i)).to eq(60)
    end
  end

describe "#create_prior_word_count_hash" do
  subject{helper.create_prior_word_count_hash(entries_array,time)}
    context "when the time is equal to or after" do
      let(:time){DateTime.new(2022,1,5,0,0,0).to_i}

      context "with one entry" do
        let(:entry1){FactoryBot.create(:entry, thoughts: thought1)}
        let(:entries_array){[entry1]}
        context "with one word in the thought" do
          let!(:thought1){'eat'}
          it "returns expected hash" do
            expect(subject).to eq({"eat"=>1})
          end
        end

        context "with two words in the thought" do
          let!(:thought1){'eat, dishes'}
          it "returns expected hash" do
            expect(subject).to eq({"eat"=>1, "dishes"=>1})
          end
        end
      end

      context "with two entries" do
        let(:entries_array){[entry1,entry2]}
        let(:entry1){FactoryBot.create(:entry, thoughts: thought1)}
        let(:entry2){FactoryBot.create(:entry, thoughts: thought2)}
        context "with one word in each thought" do
          let(:thought1){'swim'}
          let(:thought2){'swim'}

          it "returns expected hash" do
            # binding.pry
            expect(subject).to eq({"swim"=>2})
          end
        end
      end
    end
    context "when the time is before" do
      let(:time){DateTime.new(2000,1,5,0,0,0).to_i}
      context "with one entry" do
        let(:entry1){FactoryBot.create(:entry, thoughts: thought1)}
        let(:entries_array){[entry1]}
        context "with one word in the thought" do
          let!(:thought1){'eat'}
          it "returns expected hash" do
            expect(subject).to eq({})
          end
        end

        context "with two words in the thought" do
          let!(:thought1){'eat, dishes'}
          it "returns expected hash" do
            expect(subject).to eq({})
          end
        end
      end
    end
  end
end

In spec create a "models" directory and inside that directory create a file called "entry_spec.rb".

Now in spec/models/entry_spec.rb let's test the test our model.

require "rails_helper"

RSpec.describe Entry, :type => :model do
  describe "validations" do
    subject{FactoryBot.build(:entry, thoughts: thought)}
    context "when thoughts are present" do
      let(:thought){'za'}
      it "is expected to be valid" do
        expect(subject).to be_valid
      end
    end

    context "when thoughts are nil" do
      let(:thought){nil}
      it{is_expected.to be_invalid}
    end

    context "when thoughts are an empty string" do
      let(:thought){""}
      it{is_expected.to be_invalid}
    end
  end
end

OK, we are ready to writing these methods now. In app/helpers/entries_helper.rb let's crank them out.

module EntriesHelper

  def get_red(entries, thought, time)
    thought_count = create_prior_word_count_hash(entries, time)[thought]
    unless thought_count.nil?
      thought_count = thought_count *= 20
    else
      thought_count = 0
    end
    thought_count > 255 ? 255 : thought_count
  end

  def create_prior_word_count_hash(entries, time)
    thoughts_tally = {}
    _entries_prior_to_current_time(entries, time).each do |entry|
      _sanitize_thoughts(entry).each do |thought|
        if(thoughts_tally.include?(thought))
          thoughts_tally[thought] += 1
        else
          thoughts_tally[thought] = 1
        end
      end
    end
    thoughts_tally = thoughts_tally.sort_by{|k,v| -v}
    thoughts_tally.to_h
  end


  def create_all_time_word_count_hash(entries)
    thoughts_tally = {}
    entries.each do |entry|
      thoughts_array = entry.thoughts.split(',').map(&:strip).map(&:downcase)
      thoughts_array.each do |thought|
        thought = thought.lstrip
        thought = thought.rstrip
        if(thoughts_tally.include?(thought))
          thoughts_tally[thought] += 1
        else
          thoughts_tally[thought] = 1
        end
      end
    end
    thoughts_tally = thoughts_tally.sort_by{|k,v| -v}
    thoughts_tally.to_h
  end

  private
  def _entries_prior_to_current_time(entries,time)
    entries.select do |single_entry| 
      single_entry.created_at.to_i <= time
    end
  end

  def _sanitize_thoughts(entry)
    entry.thoughts.split(',').map(&:strip).map(&:downcase)
  end
end

In our entry model, lets' add some validations, so in app/models/entry.rb

class Entry < ApplicationRecord
  validates :thoughts, presence: true, length: { minimum: 2 }
end

If we run $rspec then we should get all passing tests now. You can insert a binding.pry statement in your tests or production code if you need to trouble shoot what is going on and who is getting which value. When you spin up the server and add some thoughts to the thought stream you should see the red color increase if a thoughts frequency increases.

Screen Shot 2021-04-16 at 8.14.06 PM.png

No Comments Yet