Skip to the content.

Home

Previous: Chapter 9 - The Kickoff


📝 As an app user, I want to create a selection, so that we can have a host for the next All Hands.

This is why we’re here - we’re ready to select a user! Let’s remind ourselves how the selection process should work. We will pick randomly from the pool of available engineers and mark the selection by creating a new selection record. Users are available for selection if they were created before this round was created and have not yet been selected for it.

Generate a mutation.

Terminal

bin/rails g graphql:mutation_create Selection

Sort the mutations.

app/graphql/types/mutation_type.rb

module Types
  class MutationType < Types::BaseObject
    field :round_create, mutation: Mutations::RoundCreate
    field :selection_create, mutation: Mutations::SelectionCreate
    field :user_create, mutation: Mutations::UserCreate
  end
end

Clean up the new mutation class, keeping the input type this time.

app/graphql/mutations/selection_create.rb

module Mutations
  class SelectionCreate < BaseMutation
    description "Creates a new selection"

    field :selection, Types::SelectionType, null: false

    argument :selection_input, Types::SelectionInputType, required: true

    def resolve(selection_input:)
      selection = Selection.create!(**selection_input)
      { selection: selection }
    end
  end
end

Generate the input type.

Terminal

bin/rails g graphql:input Selection

Remove every argument except round_id and make round_id required.

app/graphql/types/selection_input_type.rb

module Types
  class SelectionInputType < Types::BaseInputObject
    argument :round_id, ID, required: true
  end
end

We’re not accepting a user_id because we’re going to handle that server-side. Often we will go right to the mutation, controller, or service and add the necessary logic. However, this might be a good use case for Active Record Callbacks. When we create a selection, a user should be automatically selected. This sounds related to initialization, so let’s use after_initialize. We have other options like before_create, but this choice works well. One last thing is that callbacks can seem very magical. It can feel weird to have properties change on their own. To many, making an explicit call to a method would feel better. I used to view Active Record objects as simple objects that acted as a bridge between my input and the database. Now I view them as objects that have full control over their own properties and life cycle. If I were using some external library or even using an external api, of course it would make complete sense that creating a Selection actually makes a selection, so why should it feel weird here?

Add in the new selection logic.

app/models/selection.rb

class Selection < ApplicationRecord
  belongs_to :round
  belongs_to :user

  validates :user_id, uniqueness: { scope: :round_id }

  after_initialize do |selection|
    if selection.round.present? && selection.user.blank?
      selection.user = round.available_users.sample
    end
  end
end

Our additions to the Selection model should be pretty easy to follow. After initialization, we check to see if we need to select a user. If yes, we ask round for available_users and pick one at random using sample. Of course, to make this work we’ll need Round#available_users.

Add available_users.

app/models/round.rb

class Round < ApplicationRecord
  has_many :selections, -> { order(:id) }
  has_many :users, through: :selections

  def available_users
    User.without(users)
  end
end

The body of the method retrieves all users except the ones that are already associated with this round. users is a new relation also added here. Now that we’ve defined how things should work, let’s go through in reverse order and test them out. If something is broken, we can fix it before moving up to the next level.

Add tests for Round.

test/models/round_test.rb

require "test_helper"

class RoundTest < ActiveSupport::TestCase
  context "associations" do
    should have_many(:selections).order(:id)
    should have_many(:users).through(:selections)
  end

  context "#available_users" do
    should "return all users when none have been selected" do
      round = rounds(:empty)
      assert_equal users.sort, round.available_users.sort
    end

    should "return no users when all have been selected" do
      round = rounds(:full)
      assert_equal [], round.available_users
    end
  end
end

Run the tests.

Terminal

bin/rails t test/models/round_test.rb

Add tests for Selection.

test/models/selection_test.rb

require "test_helper"

class SelectionTest < ActiveSupport::TestCase
  context "associations" do
    should belong_to(:round)
    should belong_to(:user)
  end

  context "validations" do
    should validate_uniqueness_of(:user_id).scoped_to(:round_id)
  end

  context "after initialize" do
    should "select a random available user" do
      round = rounds(:empty)
      selection = Selection.new(round: round)
      assert_includes users, selection.user
    end

    should "not select a user when round is not set" do
      selection = Selection.new
      assert_nil selection.user
    end

    should "not select a user when one has been set" do
      round = rounds(:empty)
      user = users(:daniel)
      selection = Selection.new(round: round, user: user)
      assert_equal user, selection.user
    end

    should "not select a user when all have been selected" do
      round = rounds(:full)
      selection = Selection.new(round: round)
      assert_nil selection.user
    end
  end
end

Run the tests.

Terminal

bin/rails t test/models/selection_test.rb

Testing for true randomness can be difficult so we just wrote some pretty simple tests.

Generate a test for the mutation.

Terminal

bin/rails g integration_test types/mutation_type/selection_create

Fill in the generated test.

test/integration/types/mutation_type/selection_create_test.rb

require "test_helper"

class Types::MutationType::SelectionCreateTest < ActionDispatch::IntegrationTest
  setup do
    @query = <<~GRAPHQL
      mutation SelectionCreate($roundId: ID!) {
        selectionCreate(input: {
          selectionInput: {
            roundId: $roundId
          }
        }) {
          selection {
            id
            createdAt
            updatedAt
            user {
              id
              name
              email
              createdAt
              updatedAt
            }
          }
        }
      }
    GRAPHQL
  end

  test "selection_create" do
    round = rounds(:empty)
    variables = { roundId: round.id }

    assert_difference -> { Selection.count } do
      post graphql_path, params: { query: @query, variables: variables }

      selection = Selection.last

      assert_equal(
        {
          "data" => {
            "selectionCreate" => {
              "selection" => {
                "id" => selection.id.to_s,
                "createdAt" => selection.created_at.iso8601,
                "updatedAt" => selection.updated_at.iso8601,
                "user" => {
                  "id" => selection.user.id.to_s,
                  "name" => selection.user.name,
                  "email" => selection.user.email,
                  "createdAt" => selection.user.created_at.iso8601,
                  "updatedAt" => selection.user.updated_at.iso8601,
                },
              },
            },
          },
        },
        @response.parsed_body
      )
    end
  end

  test "selection_create with errors" do
    round = rounds(:full)
    variables = { roundId: round.id }

    assert_no_difference -> { Selection.count } do
      post graphql_path, params: { query: @query, variables: variables }

      assert_equal(
        {
          "data" => {
            "selectionCreate" => nil,
          },
          "errors" => [
            {
              "message" => "Selection invalid",
              "locations" => [{ "line" => 2, "column" => 3 }],
              "path" => ["selectionCreate"],
              "extensions" => { "user" => ["must exist"] },
            },
          ],
        },
        @response.parsed_body
      )
    end
  end
end

Run the new tests.

Terminal

bin/rails t test/integration/types/mutation_type/selection_create_test.rb

Check for regressions by running all tests.

Terminal

bin/rails t

Success!

âś… Make a commit

âś… As an app user, I want to create a selection, so that we can have a host for the next All Hands.


Next: Chapter 11 - The Reaper