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