Build a Fullstack Rails App: Part Three - TDD a the names from hat algorithm
Overview
In the previous step we set up our database and data models, so now any name drawing from hats can be persisted. But we haven't actually written the main feature of our app! We could have started with this, which really wouldn't have been a bad idea, but in this case having the data modeling set up first was also a good option, so now we can write this feature with real data.
What is an Algorithm anyway? That sound fancy.
A quick google search defines algorithm as "a process or set of rules to be followed in calculations or other problem-solving operations, especially by a computer." So this could be as complicated as a social media recommendation engine or as simple as how to randomly draw names from a hat for giving and receiving gifts.
What should our algorithm be?
First lets define what we want to accomplish first. In our database we have hats, which have drawings and names. Drawings have name matches. Name matches record a giver and receiver. So we want to input a list of names, and output name matches where everyone has a giver, and that giver isn't oneself (maybe we should've added a validation for that! We still can). Lets consider some possibilities. Most languages have some sort of random generator, so we could give it an array (like a list) of numbers and have is select one randomly. With that we could say start with a random name (with a corresponding id) and pick from the other names-ids as who that one gives to. Then remove the receiver from the list and go though the list until there are none left. We could certainly just jump in and start coding that right away! But wait a second, this still introduces a problem that drawing from hats occasionally have, which is if there are say three (or a higher odd number) people, two might draw each other, leaving the last one out. As the pool of people increase, this gets less likely, but can always still happen, if when left with the last three people, two of them give to each other. This is fine with even numbered people, but then we are simply pairing them off occasionally. Or maybe a combination of pairs and circular givings.
One solution to this is to always force a circular giving method, so no one is paired up. I think thats a good place to start. We could make things complicated and allow for pairings by an option before the drawing, but we will just stick with with a circular giving.
So how can we accomplish this? Lets give ourselves an example of how this would work: Luke -> Leia -> Han -> Chewie -> Luke
If I could type in a circle we could have just had Chewie's arrow point back around to Luke. This looks like a simple ordered list to me! [luke, leia, han, chewie] and we just know that the last gives to the first. So all we need to do is gather our list of names in an array and then shuffle them! If this were a computer science class, we might write a shuffle algorithm, and for extra credit you certainly can! But lets be real programmers and not re-invent the wheel and use ruby's built in shuffle method for arrays. Which is literally just calling shuffle
on an array. So if we had an array called names
set up like this names = [luke, leia, han, chewie]
and then simply called shuffle on that names
variable that is an Array object names.shuffle
it might give us something like [leia, chewie, han, luke]
. Then we could loop through that list and set up our name_match records by looking at the next one in the array, or the first one in the list if its the last member of the array. Now that we have a pretty good idea of what are method(s) will need to accomplish lets write some tests so that as we write our methods we have a way to know they are working, in other words, lets let our tests drive our development.
Test Driven Development (TDD) with Rspec
TDD isn't the only or the best way to write code, but it is a good way for quite a few situations. Sometimes you are still working out what a feature even needs to do or how it will work so you might not know what tests to write. That flow is pretty typical. But in our case our criteria for this feature is clear and concrete, so I think we can write the tests first.
In the previous article we added rpsec and factory bot to our project so that as we added our models it would generate the proper files to help us get started and organized. In our project root directory we have a spec
directory, this is where all our specs
or rpsec tests live. Depending on when you added rpsec, it may have generated a default tests
directory for mini-test. We won't be using that.
Anyway in spec, there should be a factories
and models
folder. Factories is from the factory-bot gem, which gives us an easy way to replicate our models and other ruby objects without having to use as much resources. For our purposes now, we are concerned with drawings, as that will be what is what will 'draw' the names from the hat, in other words create our name_match records that belong to that drawing. Go ahead and open up the spec/factories/drawings.rb file. It should look like this:
FactoryBot.define do
factory :drawing do
hat { nil }
name { "MyString" }
end
end
This shows that we can give a drawing factory a hat and name. The hat will be a hat factory object and name will default to "MyString" if we leave this as is. Which means we either need to pass it a unique name if we instantiate more than one of these, or we need to tell this factory to generate a unique name somehow. We could use a gem called Faker to generate real looking fake information, like names and make sure they are unique. Or we could simply have it sequence a number after each one is created. To keep things light, lets skip adding another gem for now (I do use Faker a lot, nothing wrong with it) and do this:
FactoryBot.define do
factory :drawing do
hat { nil }
sequence(:name) {|n| "Name #{n}" }
end
end
Now we can generate a drawing factory and it will always have a unique name by default. Save that file and lets move to actually writing a test or two in spec/models.drawing_spec.rb which looks like this to start:
require 'rails_helper'
RSpec.describe Drawing, type: :model do
pending "add some examples to (or delete) #{__FILE__}"
end
Now lets think through some of the methods we will need to write to get a drawing object able to generate the name matches we want.
I think in then end we will simply want to be able to call a method called something like draw_names
which will do all the things we want, finding the names, shuffling them, then in that shuffled order create name_match records and returning those name_matches. We don't need to test rails on whether or not drawing.names
works, or if something like drawing.names.create(giver:..., reciever:...)
works either. But the part in between where we are dealing with an array of active-record names, shuffling them and instantiating name_matches accordingly the right way would be the most interesting thing to test. So maybe we have that draw_names
method, but it calls shuffle_and_match_names(name_ids)
which returns name matches, which each name_id is a giver_id and receiver_id exactly once.
In our drawing_spec.rb file, lets start setting up our factories we will need, which will a hat, drawing, and a few names.
require 'rails_helper'
RSpec.describe Drawing, type: :model do
describe "#shuffle_and_match_names" do
let(:drawing) { create(:drawing, hat: hat) }
let(:name_1) { create(:name, hat: hat) }
let(:name_2) { create(:name, hat: hat) }
let(:name_3) { create(:name, hat: hat) }
let(:hat) { create(:hat) }
end
end
However, if your spidy-senses tingled, thats good, because we need to also set up names and hats to have unique names, especially names here since we are setting up more than one. Lets copy and paste that exact sequence name line from drawing to the hats and names factories:
FactoryBot.define do
factory :hat do
sequence(:name) {|n| "Name #{n}" }
end
end
FactoryBot.define do
factory :name do
hat { nil }
sequence(:name) {|n| "Name #{n}" }
end
end
Now that we have our factories set up, let’s write some tests. I just mentioned we want to assert that given a list of name ids, a drawing's shuffle_and_match_names method should return an array of name_matches. In that array we should expect all of the ids passed in to be both found as a receiver_id and a giver_id. Also we should expect that if we ran that method twice on the same ids, those two groups of name matches should equal each other, especially if we gave them different random seeds. Technically it’s possible that the code could work perfect and return the same exact matches, but with a different random seed that shouldn't happen typically.
I wrote out the first test, take a look and then I'll tell you more about it:
describe "#shuffle_and_match_names" do
let(:drawing) { create(:drawing, hat: hat) }
let(:name_1) { create(:name, hat: hat) }
let(:name_2) { create(:name, hat: hat) }
let(:name_3) { create(:name, hat: hat) }
let(:hat) { create(:hat) }
let(:name_ids_array) { [name_1.id, name_2.id, name_3.id] }
it "sorted returned giver_ids match sorted given array" do
shuffled_giver_ids = drawing.shuffle_and_match_names(name_ids_array).pluck(:giver_id)
expect(shuffled_giver_ids).to match_array(name_ids_array)
end
end
I added one more variable name_ids_array
to make it easier to pass that into our method. In that it
block I called the method on the drawing with the name_ids_array, then 'plucked' an array of giver_ids out of it and then wrote the expect
statement. That is what actually will give us a pass or fail. We expect that the new shuffled array giver_ids will match the name_ids_array. Using match_array
will simply compare the two array's members and disregard their order. When this passes, we know that every one is giving to someone else. Lets also test for receivers in the same way, lets add this underneath:
it "sorted returned recevier_ids match sorted given array" do
shuffled_receiver_ids = drawing.shuffle_and_match_names(name_ids_array).pluck(:reciever_id)
expect(shuffled_receiver_ids).to match_array(name_ids_array)
end
Once we have both of those passing, we know that we've got enough name matches and that everyone is giving and receiving. Cool!
Its hard to test random things, because technically random could come out as looking ordered! You could technically run a random method twice in a row and get the same result, but lets try to control for that the best we can and write a test. Maybe if we ran this test some thousands of times it would fail occasionally, but probably not as far as I know:
require 'rails_helper'
RSpec.describe Drawing, type: :model do
describe "#shuffle_and_match_names" do
let(:drawing) { create(:drawing, hat: hat) }
let(:name_1) { create(:name, hat: hat) }
let(:name_2) { create(:name, hat: hat) }
let(:name_3) { create(:name, hat: hat) }
let(:hat) { create(:hat) }
let(:name_ids_array) { [name_1.id, name_2.id, name_3.id] }
it "sorted returned giver_ids match sorted given array" do
shuffled_giver_ids = drawing.shuffle_and_match_names(name_ids_array).pluck(:giver_id)
expect(shuffled_giver_ids).to match_array(name_ids_array)
end
it "sorted returned recevier_ids match sorted given array" do
shuffled_receiver_ids = drawing.shuffle_and_match_names(name_ids_array).pluck(:receiver_id)
expect(shuffled_receiver_ids).to match_array(name_ids_array)
end
it "shuffles name matches differently with different random seeds" do
srand(1)
shuffled_1 = drawing.shuffle_and_match_names(name_ids_array).pluck(:reciever_id, :giver_id)
srand(2)
shuffled_2 = drawing.shuffle_and_match_names(name_ids_array).pluck(:reciever_id, :giver_id)
expect(shuffled_1).not_to eq(shuffled_2)
end
it "returns NameMatch objects" do
shuffled_receiver_ids = drawing.shuffle_and_match_names(name_ids_array)
expect(shuffled_receiver_ids.first).to be_a(NameMatch)
end
end
end
In our first new spec here, it calls srand
before each shuffle, which should ensure it gets two different random results. Then it directly compares the plucked array, which has both receiver and giver ids as pairs and expects that they are not equal.
For good measure I added the bottom one to make sure its returning NameMatch objects.
Now with those four tests, we should feel pretty confident our method is doing what we want it to do. Lets start writing that method, and then any others we need to finish this feature on the backend.
Write the feature methods
Lets open up our drawing.rb model file. It should have a couple relationships set up and a couple validations, but thats it. Lets start by writing the method we wrote tests for, to start by simply defining it with def shuffle_and_match_names(name_ids_array)
with a return []
and then end
in the line and two below.
This should allow us to run our spec and see it fail now. First if you haven't run rails generate rspec:install
yet you will need to do that. Then in spec/rails_helper.rb under the Rspec.configure do |config|
block add config.include FactoryBot::Syntax::Methods
. You should be ready to run bundle exec rspec ./spec/models/drawing_spec.rb
which will only run that one spec file. You could remove the file path and run all the specs as well. Either way you should see things like the expected collection contained: and the missing elements were and stuff like that, showing that it had no trouble running the tests (no syntax errors, or set up problems) and they all failed. Which is what we expect for now until we write our method. Lets write it. This is how I would think of it, by breaking it down in steps:
Use Array.shuffle to shuffle the initial name_ids_array
We need an array of NameMatches in the end, so lets initialize an empty array to add too.
Loop through the shuffled id list and initialize a new NameMatch record with the id of the current as the giver and the next as the receiver. We will have to notice if we are at the end of the array to create the last one with the first as the receiver.
return that array of name matches.
Lets set up this method with steps 1, 2, and 4 and the shell of 3.
def shuffle_and_match_names(name_ids_array)
shuffled_ids_array = name_ids_array.shuffle
name_matches = []
shuffled_ids_array.each do |name_id|
# make the name match, add it to name_matches
end
name_matches
end
You can see we set up all those variables we discussed and returned name_matches. Ruby assumes the last line in the method is what it should return if you don't explicitly write something like return name_matches
.
Now lets get to the meat of this method at look at that loop. We are going to need to know each time what the next item is, and for the last one, if we are on the last one, then find the first one. Which means we will need to know our current index
meaning which position, an arrays start at 0. With that knowledge we can add 1 to that to find the next one and check if its equal to the length of the array (we will have to -1 from that since the length will be one more than the index of the last item).
First we need to change our each
statement to each_with_index
and add the index to the enumerator between the pipes. Now there's a few ways to write this out. I've chosen to first figure out who the next_receiver_id is first with some conditions checking if this is the last item in the array or not. Then using that along with the current_giver_id. I renamed my current item I'm on as the current_giver_id
to make it clear what it is. So heres what I came up with:
def shuffle_and_match_names(name_ids_array)
shuffled_name_matches = []
shuffled_ids_array = name_ids_array.shuffle
shuffled_ids_array.each_with_index do |current_giver_id, i|
next_receiver_id = unless i == shuffled_ids_array.length - 1
shuffled_ids_array[i+1]
else
shuffled_ids_array[0]
end
shuffled_name_matches.push(name_matches.new(giver_id: current_giver_id, receiver_id: next_receiver_id))
end
shuffled_name_matches
end
In ruby you can set a variable to the result of an if then statement, so you don't have to write it over an over, which is whats going on with next_receiver_id = unless ...
Lets run rspec again bundle exec rspec ./spec/models/drawing_spec.rb
Yes! You should have gotten 4 green dots and 4 examples, 0 failures
Now we know that our method does at least those 4 things correctly at least. But what about some edge cases? Or at least can we think of ways this could break down? What if we passed it an empty array? Or something that wasn't an array? Or nothing at all? If we give it an empty array, what do we want it to do? Return an empty one back, return nil or should it error out? Well the next thing we want to do with this is save the records, or add them to that drawing to save with it. I don't think this method needs to be concerned with adding errors to the drawing object, just one thing and that is given the array return and array of name_matches. So if an empty array is given lets give an empty one back. As written it assuming name_ids_array is an array and its made very clear it should be, so we should probably just let it break whatever it breaks, and if they aren't ids of actual names, then I hold the same sentiment. We can't be much more clear about whats supposed to be passed here and I don't think we need to program it so it gives them lots of nice errors. Eventually it will break if they do it wrong, and we will let them figure that out then.
But let’s write a test and alter the method to make it clear how empty arrays are handled. Lets add a spec directly underneath what we already have to see what currently is happening and to confirm any new code adheres to what we want:
context "name_ids_array is an empty array" do
let(:name_ids_array) { [] }
it "returns an empty array" do
expect(drawing.shuffle_and_match_names(name_ids_array)).to eq []
end
end
You can see I added a context
block, which lets us say, hey here in this block we are changing the context under which this is run, and here its that our name_ids_array is now an empty array. Then we can set that array to just that just for this block.
If you run that spec, it passes. Which means our code already does what we hoped it would by happy accident, well at least according to the effort we put into it. But lets make it clear thats what supposed to happen and skip trying to anything with it if it is already empty:
def shuffle_and_match_names(name_ids_array)
return [] if name_ids_array.blank?
shuffled_name_matches = []
shuffled_ids_array = name_ids_array.shuffle
shuffled_ids_array.each_with_index do |current_giver_id, i|
next_receiver_id = unless i == shuffled_ids_array.length - 1
shuffled_ids_array[i+1]
else
shuffled_ids_array[0]
end
shuffled_name_matches.push(name_matches.new(giver_id: current_giver_id,
receiver_id: next_receiver_id))
end
shuffled_name_matches
end
end
Running rspec again should give all passing results. Now lets write some methods that will actually persist our drawing results to the database, and make it so when a drawing is created it will automatically actually draw the names.
Save Our 'Drawing' to the Database
Now that we have a tested method to handle randomly drawing names, lets write a method that will call that one and then save those names.
This method will rely on our database structure and being opinionated on it being setup in that way. While the shuffle and match names only needed this class to have a 'name_matches' relationship. In this method we will need access the the correct list of names that the hat has, so lets make a relationship for that has_many :names, through: :hat
in the line below the belongs_to :hat
relationship. Now we can call names
on a drawing instance and get the hats names, which is the only names we want to deal with for this method. It will also make this method trivial:
def draw_and_save_matches
name_matches = shuffle_and_match_names(names.pluck(:id))
name_matches.each(&:save)
end
Or a one liner:
def draw_and_save_matches
shuffle_and_match_names(names.pluck(:id)).each(&:save)
end
Now we don't have to test whether rails relationships and save features work correctly as we've put them in this separate method. However, I wouldn't complain to much if someone combined all of this into one, and skipped plucking the ids and just iterated over the names it had access too. I just like this (for now) to make it simpler to test our 'algorithm'
We can also set this up to run each time we create a new drawing. We could debate weather we should create an 'after create callback' for this or just remember to call our draw_and_save_matches each time we want to. In a lot of, if not most situations, it is a really good idea to consider not creating a callback, but this is a situation where the existence of a 'drawing' only matters if names were drawn, right? So a drawing shouldn't exist in our database without names_matches attached, and also without the hat having at least three names. So lets add those constraints.
after_create :draw_and_save_matches
def draw_and_save_matches
if names.size < 3
errors.add(:base, "Hat must have at least 3 names")
return
end
shuffle_and_match_names(names.pluck(:id)).save
end
Above our methods and below our validations I added that after_create
callback, which simply calls our draw_and_save_matches
method every time a new record is created for the first time. I've also added a validation as a guard statement in draw_and_save_matches
to make sure there are at least 3 names. When we get to the front end we may come up with a better way to do this validation, but at least its in place for now.
Lets try out creating a drawing in our console to see if it creates the name_matches like we expect. First just to make all our data consistent, I'm going to delete any previous drawings and corresponding name_matches with Drawing.destroy_all
. Because we have dependent: :destroy
on our has_many
relationships destroying the drawing will destroy its dependents, which are all the name_matches. Now you can find your Hat with names we've already created, for me Hat.first
found it, if that returns something, you can call Hat.first.drawings.create(name: "1978")
(or whatever name you want) and it should create not just the drawing, but all the corresponding name_matches. Your console should give some output like this:
This saved a drawing that belongs to the first found Hat, and because we have an after_create callback for draw_and_save_matches
it automatically runs it, which shuffles all the names that belong it it's hat and then creates name match records for us. The intial console output does show the random drawing order, but if we type Hat.first.drawings.first.name_matches
it will print it in a easier to read way like this:
You can see by id that 1 gives to 3 who gives to 2 who gives to 4 who gives to 1. Those ids correspond to the names we setup earlier. We could create another drawing to see if its random (which we know it will be based on our specs we wrote, but just for fun). I tried it and sure enough it gave a new order. You could try adding or removing a name from the hat and see how it performs as well. I feel satisfied with what we've got so far. We could type a little script to show us the names, but we will just wait until we get to the front end code to really show it nicely in the browser.
We did it! We wrote the models and business logic for our name from hat drawing app! Now all we need to do is create a web interface to make it usable in a browser.