The problem
From labs/2-salespeople.rb in your copy of models-and-tests:
You write the rules in plain English; your agent writes the Ruby. Needs lab 1 first.
Four steps: the rules in DONE.md, a prompt that gets tests for them and nothing else, your check of those tests, then the code and a script. Your agent's Ruby won't match what's below line for line, and it shouldn't. What should match is the shape: every sentence has a test, each test checks its sentence, and the tests failed before the model changed.
1. The rules
Under Salesperson in DONE.md, one sentence per line. The lab suggests two questions, and each becomes a rule:
## Salesperson
A salesperson can't be saved without a first name.
Two salespeople can't have the same email.
You could write more. "A salesperson can't be saved without an email" is a reasonable third. Every sentence you write is a rule the app will enforce, so write the ones you mean. A rule you don't write down isn't a rule, and lab 4 is about what that costs.
2. The prompt
Code-along 3's prompt, with one word changed:
My rules for Salesperson are under Salesperson in DONE.md. Write a test for each one, run them and show me they fail. Don't change the model yet.
"Don't change the model yet" is the important half. Without it, an agent will happily write the tests and the code in one go and show you green. Green on the first run tells you nothing: a test you have never seen fail might not be testing anything.
The agent should write something like this in test/models/salesperson_test.rb:
require "test_helper"
class SalespersonTest < ActiveSupport::TestCase
# One test for each sentence under Salesperson in DONE.md, named with that sentence.
test "a salesperson can't be saved without a first name" do
salesperson = Salesperson.new
salesperson["last_name"] = "Block"
salesperson["email"] = "ben@example.com"
salesperson.save
assert_equal 0, Salesperson.all.count
end
test "two salespeople can't have the same email" do
salesperson1 = Salesperson.new
salesperson1["first_name"] = "Ben"
salesperson1["email"] = "ben@example.com"
salesperson1.save
salesperson2 = Salesperson.new
salesperson2["first_name"] = "Brian"
salesperson2["email"] = "ben@example.com"
salesperson2.save
assert_equal 1, Salesperson.all.count
end
end
And it should run them and show you this:
Failure:
SalespersonTest#test_a_salesperson_can't_be_saved_without_a_first_name
Expected: 0
Actual: 1
Failure:
SalespersonTest#test_two_salespeople_can't_have_the_same_email
Expected: 1
Actual: 2
2 runs, 2 assertions, 2 failures, 0 errors, 0 skips
Read the failures. The first says a salesperson with no first name was saved: one row where there should be none. The second says both Bens were saved. Both rules are broken, which is right, because nothing enforces them yet.
3. Check its work
Before asking for the code, open DONE.md and the test file side by side.
- Count. Two sentences, two tests. A sentence with no test is a rule your app will never enforce, however sure the agent sounded when it said "done".
- Match the names. Each test is named with its sentence, so you can pair them by reading.
- Read each test. Does it do what the sentence says? The first test leaves out the first name and expects nothing saved. The second saves the same email twice and expects one row. A test that set the first name and then expected a save to fail would be checking the wrong thing, and would stay red no matter what the model did.
An agent will sometimes test a rule you didn't write (a presence check on email, say). That isn't wrong, but it isn't yours either: either add the sentence to DONE.md or ask for the test to go.
Then:
Make them pass.
4. The model
One line per rule:
class Salesperson < ApplicationRecord
validates :first_name, presence: true
validates :email, uniqueness: true
end
validates is how a model states a rule. presence: true refuses to save a row with that column blank. uniqueness: true looks in the table before saving and refuses if the value is already there. When a rule fails, save returns false instead of saving, and errors.full_messages says why: Email has already been taken.
Run them yourself, not just the agent:
bin/rails test
2 runs, 2 assertions, 0 failures, 0 errors, 0 skips
(If you run the whole suite, the Company and Contact tests from class run too.)
5. The script
Ask for scripts/salespeople.rb: delete every salesperson, create two, print the count and each full name.
# Start empty, so this can run more than once
Salesperson.destroy_all
ben = Salesperson.new
ben["first_name"] = "Ben"
ben["last_name"] = "Block"
ben["email"] = "ben@example.com"
ben.save
brian = Salesperson.new
brian["first_name"] = "Brian"
brian["last_name"] = "Eng"
brian["email"] = "brian@example.com"
brian.save
puts "Salespeople: #{Salesperson.all.count}"
for salesperson in Salesperson.all
puts "#{salesperson["first_name"]} #{salesperson["last_name"]}"
end
bin/rails runner scripts/salespeople.rb
Salespeople: 2
Ben Block
Brian Eng
The destroy_all at the top is what lets you run it twice. Without it, the second run would try to save a second ben@example.com, your uniqueness rule would refuse it, and the count would stay at 2 while the script quietly skipped a row. Comment it out and run the script twice to see that happen.
Challenge: change an email after it's saved
ben["email"] = "bblock@example.com"
ben.save
Before you run it: what should the output show? The same three lines. The script never prints an email, so a changed email changes nothing you can see. That's the point of the question: the data changed, and the output didn't tell you.
To see the change, print the email, or look at the Data page in the browser, or ask the database:
puts Salesperson.find_by({ "email" => "ben@example.com" }).inspect # nil
puts Salesperson.find_by({ "email" => "bblock@example.com" })["first_name"] # Ben
The first is nil: nobody has the old email any more. Keep that in mind for lab 3, which looks people up. Look them up by something that doesn't change, like a name, or by the current email.