TDD Fundamental cleanup exercise

I’m trying to refactor and do a cleanup exercise from this repo and this is my solution:

RSpec.describe Invitation do
  # this is the helper
  def create_user_and_build_invitation
    team_owner = User.create 
    team = Team.create name: "A fine team", owner: team_owner
    team_owner.update team: team
    new_user = User.create(email: 'rookie@example.com')

    [new_user, Invitation.new(team: team, user: new_user)]
  end
  
  # some lines below are deleted
  describe "callbacks" do
    describe "after_save" do
      context "with valid data" do
        it "invites the user" do
          new_user, invitation = create_user_and_build_invitation
          invitation.save
          expect(new_user).to be_invited
        end
      end

      context "with invalid data" do
        it "does not save the invitation" do
          new_user, invitation = create_user_and_build_invitation
          invitation.team = nil
          invitation.save

          expect(invitation).not_to be_valid
          expect(invitation).to be_new_record
        end

        # ...
      end
    end
  end

  describe "#event_log_statement" do
    context "when the record is saved" do
      it "include the name of the team" do
        new_user, invitation = create_user_and_build_invitation
        invitation.save
        log_statement = invitation.event_log_statement
        expect(log_statement).to include("A fine team")
      end

      # ...
    end

    context "when the record is not saved but valid" do
      it "includes the name of the team" do
        new_user, invitation = create_user_and_build_invitation
        log_statement = invitation.event_log_statement
        expect(log_statement).to include("A fine team")
      end

      # ...
    end

    context "when the record is not saved and not valid" do
      it "includes INVALID" do
        new_user, invitation = create_user_and_build_invitation
        invitation.user = nil
        log_statement = invitation.event_log_statement
        expect(log_statement).to include("INVALID")
      end
    end
  end
end

and I have some questions:

  1. Do you have any advice to improve the code above?
  2. Do I need to split the helper into two: one for generating the invitation and the second for generating the new_user?
  3. Why this cleanup can decrease test running time? I mean the helper is creating some variable and called in each test anyway so I don’t feel like this brings more performance to the test (as far as I know, and I am new to rspec and TDD)
    Thanks for your time and assistance. I truly appreciate your help and insights.

I believe the goal with this section was more around making the specs more readable and easier to follow.

One thing I noticed is that we are asserting things around “Invitation”, particularly the after_save callback, so the outermost before block that is updating team and team_owner are not needed - we can remove team_owner as well!

Therefore I think all we need is a build_invitation method that’s return an invitation record:

def build_invitation
  user = User.new(email: "rookie@example.com")
  team = Team.new(name: "A fine team")
  
  Invitation.new(team: team, user: user) 
end

By defining only the method above, and making the necessary adjustments, all the specs will pass :slight_smile:

Here’s my refactored code:

# spec/factories.rb
FactoryBot.define do

  factory :invitation do
    user
    team
  end

  factory :user do
    email { "john_doe@example.com" }
    invited { false }
    team
  end

  factory :team do
    name { "Backend Devs" }
  end

  factory :magazine do
    name { "The Ruby Times" }
  end

  factory :person do
  end

  factory :subscription do
    person
    magazine
  end

end
# spec/support/helpers/invitation_helper.rb
module Helpers
  module InvitationHelper
    def build_invitation_with(team_name: nil, user_email: nil)
      invitation = build(:invitation)

      invitation.team = build(:team, name: team_name) if team_name
      invitation.user = build(:user, email: user_email) if user_email

      invitation
    end
  end
end
# spec/models/invitation_spec.rb
require "rails_helper"

RSpec.configure do |c|
  c.include Helpers::InvitationHelper
end

RSpec.describe Invitation do
  describe "callbacks" do
    describe "after_save" do
      context "with valid data" do
        it "invites the user" do
          new_user = create(:user)
          invitation_for_new_user = build(:invitation, user: new_user, team: new_user.team)

          invitation_for_new_user.save

          expect(new_user).to be_invited
        end
      end

      context "with invalid data" do
        it "does not save the invitation" do
          invalid_invitation = build(:invitation, team: nil)

          invalid_invitation.save

          expect(invalid_invitation).not_to be_valid
          expect(invalid_invitation).to be_new_record
        end

        it "does not mark the user as invited" do
          new_user = create(:user)
          invalid_invitation_for_new_user = build(:invitation, user: new_user, team: nil)

          invalid_invitation_for_new_user.save

          expect(new_user).not_to be_invited
        end
      end
    end
  end

  describe "#event_log_statement" do
    context "when the record is saved" do
      it "includes the name of the team" do
        invitation = build_invitation_with(team_name: "A fine team")

        log_statement = invitation.event_log_statement

        expect(log_statement).to include("A fine team")
      end

      it "includes the email of the invitee" do
        invitation = build_invitation_with(user_email: "rookie@example.com")

        log_statement = invitation.event_log_statement

        expect(log_statement).to include("rookie@example.com")
      end
    end

    context "when the record is not saved but valid" do
      it "includes the name of the team" do
        invitation = build_invitation_with(team_name: "A fine team")

        log_statement = invitation.event_log_statement

        expect(log_statement).to include("A fine team")
      end

      it "includes the email of the invitee" do
        invitation = build_invitation_with(user_email: "rookie@example.com")

        log_statement = invitation.event_log_statement

        expect(log_statement).to include("rookie@example.com")
      end

      it "includes the word 'PENDING'" do
        invitation = build(:invitation)

        log_statement = invitation.event_log_statement

        expect(log_statement).to include("PENDING")
      end
    end

    context "when the record is not saved and not valid" do
      it "includes INVALID" do
        invalid_invitation = build(:invitation, user: nil)

        log_statement = invalid_invitation.event_log_statement

        expect(log_statement).to include("INVALID")
      end
    end
  end
end

Note: I also changed config.use_transactional_fixtures = false to config.use_transactional_fixtures = true in spec/rails_helper.rb file so that any data created within an example will be run within a transaction and rolled back.

Any advice for improvement would be appreciated. Thanks!