Cards Against Isolation (Complete Devise Testing)

In the last post, we tested authentication but we didn’t actually complete all the Devise related testing. We really need to make sure that players can register and reset their password if they forget it.

Registration#

We have a spec file, spec/features/player_authentication_spec.rb, for testing authentication so let’s start by making a similar file for registration at spec/features/player_registration_spec.rb:

# frozen_string_literal: true

describe "Player registration", type: :feature do
end

The first thing we are going to want to know is whether pressing the “Sign up” button on the sign-in page takes the player to the registration page.

describe "Player registration", type: :feature do
  context "when a player clicks the registration link" do
    it "takes them to the registration page" do
    end
  end
end

I’ve seen people really overthink those stories. Don’t! You can always modify it later. Now is the time to feel things out. Wait until after your test is passing to worry about the little details.

We know that this story starts on the sign-in page, so we’ll need to load that page. There will need to be an element for the registration link that needs to be clicked. And we need a concept of a registration page to check that clicking the link took us there.

it "takes them to the registration page" do
  signin_page = Pages::Players::SignIn.new
  signin_page.load

  signin_page.registration_link.click

  registration_page = Pages::Players::Registration.new
  expect(registration_page).to be_displayed
end

Okay, you should know the deal by now.

  1. Run the test
  2. Read the error
  3. Correct the error
  4. Repeat

One thing to note is that we now have multiple test files. You don’t need to run all of your tests during the build process. You absolutely should run all of your tests when you believe you have completed a feature but you’ll want to get quick responses from RSpec until that point.

You can run just one file by appending the file path to the command like:

rspec spec/features/player_registration_spec.rb

You can also include a line number for the test (any line number from the it to the end will do) to only run one test in the file:

rspec spec/features/player_registration_spec.rb:4

I would recommend you find a shortcut that suits your workflow. I like to use the “Rails Run Specs” extension for VS Code. Code has a built-in terminal and “Rails Run Specs” adds shortcuts for running your tests in the terminal:

  • command-shift-t : run all specs in the file
  • command-l : run only the spec/s for the selected line
  • command-y : re-run the last spec that was run

In the past, I’ve used the Guard gem with both Guard::RSpec and guard-rubocop. Guard runs in its own terminal window and will watch you to see when you make file changes.

You designate tasks to be performed in response to file changes and those tasks can be reasonably intelligent. For instance, the Rubocop task will only check your changes, not the whole project, and the RSpec task can work out which spec file relates to the changes and run just those specs. I still really like Guard but, when you are working on a team, the team needs to agree to it whereas you can add an extension to your editor without disrupting others.

Test error:

Failure/Error: signin_page.registration_link.click

NoMethodError:
  undefined method `registration_link'

The variable, signin_page, which is an instance of Pages::Players::SignIn, doesn’t yet know about the registration link, so let’s add it:

element :login_button, "#new_player input[type=submit]"

element :registration_link, "#new_player FIX_ME"

element :alert, ".alert"

Devise doesn’t make it easy for us to find this link. The HTML displayed on the page is simply:

<a href="/players/sign_up">Sign up</a>

so we can either find a link with the href “/players/sign_up” or where the text reads “Sign up”. Generally, I wouldn’t like to hook into those attributes because they are implementation details. We only want to test that pressing the registration link takes us to the registration page, whether the text says “Sign up” or “Register” or if the link is “/players/sign_up” or “/register” really doesn’t matter. Devise does offer a way to override the default templates but we should test that things work first and then we can refactor later. If we were to change things now and the functionality was broken, we wouldn’t know if we broke it in the refactor or it was never working.

The easiest attribute to find will be the path, or href. In the same way we used the Rails routes to find the path of the sign-in page, we can find the path for registration.

Running rails routes includes:

PrefixVerbURI PatternController#Action
new_player_registrationGET/players/sign_up(.:format)devise/registrations#new

This suggests we could find the link by using:

element :registration_link, "a[href='#{new_player_registration_path}']"

That doesn’t quite work, though, I’m afraid. Previously we saw that syntax in a test file. It comes from the Rails URL helpers which RSpec loads in our tests and Rails loads into controllers, but the helpers aren’t automatically available everywhere. That doesn’t mean we can’t use them; it just requires a little more code.

The path methods come from the Rails URL helpers which are available from Rails.application.routes.url_helpers; this means we can get the new_player_registration_path from Rails.application.routes.url_helpers.new_player_registration_path. That’s a hell of a lot to read, though, which makes it hard to quickly understand what is going on. Instead, we can create a method with a descriptive name:

module Pages
  module Players
    class SignIn < SitePrism::Page
      class << self
        private

        def registration_path
          url_helpers = Rails.application.routes.url_helpers
          url_helpers.new_player_registration_path
        end
      end

      set_url "/players/sign_in"

      element :email_field, "#new_player input[name='player[email]']"
      element :password_field, "#new_player input[name='player[password]']"
      element :login_button, "#new_player input[type=submit]"

      element :registration_link, "a[href='#{registration_path}']"

      element :alert, ".alert"
    end
  end
end

We call element inside the class, rather than inside a method, which means it is run in the class scope:

Pages::Players::SignIn.element # this method exists

signin_page = Pages::Players::SignIn.new
signin_page.element # this method does not

This means the method we create needs to be in the class scope otherwise we need to call new like new.registration_path. One way to create a method in the class scope is to prefix the name with self.:

def self.registration_path
  url_helpers = Rails.application.routes.url_helpers
  url_helpers.new_player_registration_path
end

This creates a public class method called registration_path:

Pages::Players::SignIn.registration_path
# "/players/sign_up"

That would work for our needs but there are two things to consider:

  1. once a method is public, you should consider that you are now dedicated to maintaining it and avoiding changing its interface; and
  2. the SignIn page class is now publically responsible for providing the URL for the registration page which really isn’t in any way connected with its reason for existing.

By making the method private, we can use it in our class without creating an expectation that another class can rely upon it. Unfortunately, the standard private method call doesn’t work with class methods. Saying this, class methods are actually just instance methods of the singleton class and instance methods can be made private.

Woah, woah, woah… the what‽

Languages that are inspired by Smalltalk, Ruby included, have the concept of a “metaclass”. The class of a class is this metaclass or singleton class. When you create a class method, they go into this singleton class. If you are unfamiliar with this concept, you absolutely should take the time to watch Nadia Odunayo’s talk The Case Of The Missing Method - A Ruby Mystery Story. It is absolutely one of the best talks I’ve ever seen and it takes this concept that I just made sound super-confusing and explains it in a way that is fun and easy to understand.

To access the singleton class we use the syntax:

class << self
end

and in there we can call the private method:

class << self
  private
end

so that, when we add our new method:

class << self
  private

  def registration_path
    ...
  end
end

it is available in the class scope but it isn’t being presented as a trustworthy method that can be relied upon.

I think it would be nicer if the method definition came after the call to element. Unfortunately, Ruby doesn’t first parse the file and then run it, it will execute the request to get the registration_path when it sees it. If the method hasn’t been defined before it is called, an error will be raised.

Test error:

Failure/Error: registration_page = Pages::Players::Registration.new

NameError:
  uninitialized constant Pages::Players::Registration

We create the bare-bones registration page class at spec/pages/players/registration.rb:

# frozen_string_literal: true

module Pages
  module Players
    class Registration < SitePrism::Page
    end
  end
end

Test error:

Failure/Error: expect(registration_page).to be_displayed

SitePrism::NoUrlMatcherForPageError:
  SitePrism::NoUrlMatcherForPageError

Set the url that we found earlier:

class Registration < SitePrism::Page
  set_url "/players/sign_up"
end

This test should be passing. Now that we know we can get to the registration page, let’s make sure we can actually register. The first step in registration will be submitting the form and checking that a player is added to the database.

Sometimes, in the excitement of getting a green test, you can forget to test the unhappy path. Not only can you create more than one test at a time, you can also write out the stories without any expections and RSpec will remind you that you have pending tests. So, let’s write out a few stories:

context "when the registration form is filled in correctly" do
  it "creates a new, unconfirmed player" do
  end
end

context "when the email address is missing" do
  it "shows an error message"
end

context "when the email address is invalid" do
  it "shows an error message"
end

context "when the password is missing" do
  it "shows an error message"
end

context "when the password is too short" do
  it "shows an error message"
end

context "when the password confirmation is missing" do
  it "shows an error message"
end

context "when the password confirmation does not match the password" do
  it "shows an error message"
end

Running your tests now will show 6 pending specs. The one we will work on now includes a do/end block so it is not counted.

Now that we’ve protected ourselves from ourselves, we can fill in our first expectation:

context "when the registration form is filled in correctly" do
  it "creates a new, unconfirmed player" do
    registration_page = Pages::Players::Registration.new
    registration_page.load

    registration_page.email_field.set("player@example.com")
    registration_page.password_field.set("Passw0rd")
    registration_page.password_confirmation_field.set("Passw0rd")

    expect { registration_page.register_button.click }.
      to change(Player, :count).by 1

    new_player = Player.find_by(email: "player@example.com")
    expect(new_player.confirmed?).to be false

    signin_page = Pages::Players::SignIn.new
    expect(signin_page).to be_displayed
  end
end

The first 5 lines are the same as we have seen previously. The next bit:

expect { registration_page.register_button.click }.
  to change(Player, :count).by 1

is making sure that a new player is added to the database. We place registration_page.register_button.click inside a block because we need to be sure that it isn’t run until RSpec is ready. Before executing that code, RSpec needs to check how many players are in the database. It does this before and after running the code in the block to see if the value changes.

If you wanted to know how many players are in the database, you would run Player.count. Of course, if we had written change(Player.count), we would be sending the message “0” to the change method. Instead, we need to tell RSpec what message to send so it can run it before and after.

Then we want to know that, while the player is in the database, they have not been confirmed:

new_player = Player.find_by(email: "player@example.com")
expect(new_player.confirmed?).to be false

That new_player.confirmed? was an educated guess that turned out to be correct but Devise does list it in the documentation for confirmable.

And finally, we redirect back to the sign-in page.

Test error:

Failure/Error: registration_page.email_field.set("player@example.com")

NoMethodError:
  undefined method `email_field'

Interestingly, Devise uses the same ID for the form on the registration page as the sign-in page, new_player, and it continues to use the Rails form naming conventions. You can assume that each input element will have a name attribute of model[attribute] or, in this case, player[email]:

class Registration < SitePrism::Page
  set_url "/players/sign_up"

  element :email_field, "#new_player input[name='player[email]']"
end

Test error:

Failure/Error: registration_page.password_field.set("Passw0rd")

NoMethodError:
  undefined method `password_field'

Following the convention:

element :email_field, "#new_player input[name='player[email]']"
element :password_field, "#new_player input[name='player[password]']"

Test error:

Failure/Error: registration_page.password_confirmation_field.set("Passw0rd")

NoMethodError:
  undefined method `password_confirmation_field'

Again, but over two lines else we will surpass our 80 character line limit and the cop will be quite upset:

element :password_confirmation_field,
        "#new_player input[name='player[password_confirmation]']"

Test error:

Failure/Error:
  expect { registration_page.register_button.click }.
    to change(Player, :count).by 1

NoMethodError:
  undefined method `register_button'

Fixed by:

element :password_confirmation_field,
        "#new_player input[name='player[password_confirmation]']"
element :register_button, "#new_player input[type=submit]"

And we’re passing already; how simple was that‽

I don’t want to over-optimise but it is inevitable that every test in this file is going to need an instance of Pages::Players::Registration so I’d like to extract that into a shared variable:

describe "Player registration", type: :feature do
  let(:registration_page) { Pages::Players::Registration.new }

  context "when a player clicks the registration link" do
  • Run the tests to make sure nothing has broken;
  • remove the two lines that say registration_page = Pages::Players::Registration.new; then
  • run the tests again.

Now we can fill in the bodies of the remaining tests. I know from testing the page manually that Devise uses its own error messages for this page, not the Rails flash messages that are used for sign-in. I’m not really sure why this discrepancy exists. These errors are only shown when there is an issue related to the form submission and, as such, as can simply test that an error is shown, not what the error is. This goes back to the idea that we are not testing implementation details—we are not interested in the specific words shown, only that the expected reaction occurs.

context "when the email address is missing" do
  it "shows an error message" do
    registration_page.load

    registration_page.password_field.set("Passw0rd")
    registration_page.password_confirmation_field.set("Passw0rd")
    registration_page.register_button.click

    expect(registration_page).to have_error_container
  end
end

context "when the email address is invalid" do
  it "shows an error message" do
    registration_page.load

    registration_page.email_field.set("not and email address")
    registration_page.password_field.set("Passw0rd")
    registration_page.password_confirmation_field.set("Passw0rd")
    registration_page.register_button.click

    expect(registration_page).to have_error_container
  end
end

context "when the password is missing" do
  it "shows an error message" do
    registration_page.load

    registration_page.email_field.set("player@example.com")
    registration_page.password_confirmation_field.set("Passw0rd")
    registration_page.register_button.click

    expect(registration_page).to have_error_container
  end
end

context "when the password is too short" do
  it "shows an error message" do
    registration_page.load

    registration_page.email_field.set("player@example.com")
    registration_page.password_field.set("Passw")
    registration_page.password_confirmation_field.set("Passw")
    registration_page.register_button.click

    expect(registration_page).to have_error_container
  end
end

context "when the password confirmation is missing" do
  it "shows an error message" do
    registration_page.load

    registration_page.email_field.set("player@example.com")
    registration_page.password_field.set("Passw0rd")
    registration_page.register_button.click

    expect(registration_page).to have_error_container
  end
end

context "when the password confirmation does not match the password" do
  it "shows an error message" do
    registration_page.load

    registration_page.email_field.set("player@example.com")
    registration_page.password_field.set("Passw0rd")
    registration_page.password_confirmation_field.set("This does not match")
    registration_page.register_button.click

    expect(registration_page).to have_error_container
  end
end

Most of this should look familar to you now apart from:

expect(registration_page).to have_error_container

This is simply saying that an element called error_container will be somewhere on the page. The error container is only shown when there is an error, otherwise it doesn’t exist on the page.

Test error:

Failure/Error: expect(registration_page).to have_error_container
  expected #<Pages::Players::Registration:0x00007f91891ac440> to respond to `has_error_container?`

Of course, since we haven’t told SitePrism about an element called “error_container”, the test will fail. From inspecting the page, I can tell you that the container has an ID of error_explanation so the element added as:

element :register_button, "#new_player input[type=submit]"

element :error_container, "#error_explanation"

And, like that, we now have 8 passing tests. It can take a while to get some momentum but, once you get those foundations in place, things can really move.

So we know that an unconfirmed user is added to the database but how do we simulate receiving an email and clicking the confirmation link? The answer isn’t obvious; I’ve learnt to answer questions like these through much Googling over the years. What I can say, however, is that the answer should always start with a test.

Since this is the step after the existing happy path, let’s keep it in the same context:

context "when the registration form is filled in correctly" do
  it "creates a new, unconfirmed player" do
    ...
  end

  context "when the player clicks the confirmation link in their email" do
    it "confirms their account" do
    end
  end
end

To test this, we need a player that has registered. We could fill in the registration form again but we have already proven that works in another test so we don’t want to test it again. Instead, we can just create an unconfirmed player like we did in the authentication spec:

context "when the player clicks the confirmation link in their email" do
  let(:unconfirmed_player) do
    Player.create!(
      email: "player@example.com",
      password: "Passw0rd"
    )
  end

  it "confirms their account" do
    unconfirmed_player
  end
end

We can’t (easily) simulate receiving an email and clicking a link but there would surely be a way to ask Devise for that link. We know that a link can only work if Rails has been told about it in the routes so that’s a great place to start (rails routes):

PrefixVerbURI PatternController#Action
new_player_confirmationGET/players/confirmation/new(.:format)devise/confirmations#new
player_confirmationGET/players/confirmation(.:format)devise/confirmations#show
_POST/players/confirmation(.:format)devise/confirmations#create

Those are all the routes relating to confirming a player. Since the confirmation occurs after a link is clicked in an email, we can be reasonably certain that the verb will be “GET”. By default, links are always going to be GET requests. In a website, it is not only possible to change this but Rails does this to allow things like deleting records but you can safely assume that a link in an email is going to be a GET whereas POST is going to be the submission of a form.

That leaves us with new_player_confirmation, and player_confirmation. The new action should show you a page for creating something so we can expect that it won’t have any knowledge of our specific confirmation. That leaves us with the show action. It certainly isn’t usual for a show action to perform a side-effect but it would need to relate to a specifc confirmation and would generally take some kind of identifier. If this was a Rails controller backed by an object in the database, the show links would be like model_name/id_of_thing.

Because I know that there needs to be an identifier and I can’t see one in the URI pattern, I jumped into the Devise source code to move forward. I fully appreciate that this isn’t the simpliest path for people who are not yet comfortable navigating Ruby projects. Another way to solve this problem would be to complete the registration form in the browser and check the link in the email but we haven’t configured the ability to send emails yet.

The results from the routes said that the controller is devise/confirmations. Since Devise is specifically for Rails, I had an expectation that it would follow Rails conventions and so I expected to find a ConfirmationsController inside app/controllers just like how my own controllers are defined. When I opened app/controllers on GitHub I could see a devise directory and inside that directory was confirmations_controller. That file has a show method which has this helpful comment above:

# GET /resource/confirmation?confirmation_token=abcdef

We now know that we need to send a request to the player_confirmation_path and there needs to be a confirmation_token in the URL parameters. The URL parameter can be passed to the path like player_confirmation_path(confirmation_token: some_token).

So, where do we get that token? Thankfully, Devise puts it on the player with exactly that name (unconfirmed_player.confirmation_token).

Putting it all together:

it "confirms their account" do
  confirmation_page = Pages::Players::Confirmation.new
  token = unconfirmed_player.confirmation_token

  confirmation_page.load(token: token)
end

Because we are calling unconfirmed_player when we load the page, we no longer need to call it at the start of the test.

To be certain that the player is being transitioned from unconfirmed to confirmed, we will run our expectation in a block like we did in it "creates a new, unconfirmed player" do:

it "confirms their account" do
  confirmation_page = Pages::Players::Confirmation.new
  token = unconfirmed_player.confirmation_token

  expect { confirmation_page.load(token: token) }.
    to change { unconfirmed_player.reload.confirmed? }.to true
end

Previously when we used the change method we passed the parameters in parentheses (change(Player, :count)) but now we are using a block. This is because the first option only supports one method call and we are chaining two methods. But why are we doing that? We create the player and store it to a variable. When Devise confirms the player, it puts a date into the confirmed_at column in the database. Our unconfirmed_player variable doesn’t know that happened. unconfirmed_player is essentially a cache of what the database looked like at the point we set the variable. When we callreload, Active Record goes back to the database and updates the cache.

Test error:

Failure/Error: confirmation_page = Pages::Players::Confirmation.new

NameError:
  uninitialized constant Pages::Players::Confirmation

Create the page class, spec/pages/players/confirmation.rb:

# frozen_string_literal: true

module Pages
  module Players
    class Confirmation < SitePrism::Page
    end
  end
end

Test error:

Failure/Error:
  expect { confirmation_page.load(token: token) }.
    to change { unconfirmed_player.reload.confirmed? }.to true

SitePrism::NoUrlForPageError:
  SitePrism::NoUrlForPageError

Add the URL to the page class:

class Confirmation < SitePrism::Page
  set_url "/players/confirmation?confirmation_token={token}"
end

That “{token}” is how SitePrism allows us to pass parameters into the URL and is what makes confirmation_page.load(token: token) work.

And with that test complete, we have completed testing registration so we should commit:

rubocop
rspec
git add --intent-to-add spec
git add --patch
git commit

Add tests for player registration

Forgot password#

Now that we have completed registration, this should be a breeze. We have all the knowledge required to fly though it, it’s just a matter of applying that knowledge to a different page.

As with registration, we start by creating a spec file at spec/features/player_forgotten_password_spec.rb:

# frozen_string_literal: true

describe "Player forgotten password", type: :feature do
end

The registration spec began:

let(:registration_page) { Pages::Players::Registration.new }

context "when a player clicks the registration link" do
  it "takes them to the registration page" do
    signin_page = Pages::Players::SignIn.new
    signin_page.load

    signin_page.registration_link.click

    expect(registration_page).to be_displayed
  end
end

so let’s reword that for forgotten password:

let(:forgotten_password_page) { Pages::Players::ForgottenPassword.new }

context "when a player clicks the forgotten password link" do
  it "takes them to the forgotten password page" do
    signin_page = Pages::Players::SignIn.new
    signin_page.load

    signin_page.forgotten_password_link.click

    expect(forgotten_password_page).to be_displayed
  end
end

If you are calling RSpec manually, now you will want to call:

rspec spec/features/player_forgotten_password_spec.rb

Test error:

Failure/Error: signin_page.forgotten_password_link.click

NoMethodError:
  undefined method `forgotten_password_link'

Before we can create the forgotten_password_link element, we need to work out what the path is. Running rails routes shows us:

PrefixVerbURI PatternController#Action
new_player_passwordGET/players/password/new(.:format)devise/passwords#new
edit_player_passwordGET/players/password/edit(.:format)devise/passwords#edit
player_passwordPATCH/players/password(.:format)devise/passwords#update
_PUT/players/password(.:format)devise/passwords#update
_POST/players/password(.:format)devise/passwords#create

passwords#new is the most likely candidate here since we want a page that will allow us to request a new password. Let’s look at the other options, though.

edit and update do seem like they could make sense but, apart from the fact we can look at the URL in the browser which shows /players/password/new, we would expect that the page we are looking for will let us request an email with a link we can click to get to a page to update our password. Since the final action is updating the password and you update a record from an edit page, it seems most likely that the page we are looking for creates an edit link.

As with the confirmation link, we can exclude the create action since POST actions should create records and we only want to load a page.

Add the forgotten_password_link element to Pages::Players::SignIn:

class << self
  private

  def registration_path
    url_helpers = Rails.application.routes.url_helpers
    url_helpers.new_player_registration_path
  end

  def forgotten_password_path
    url_helpers = Rails.application.routes.url_helpers
    url_helpers.new_player_password_path
  end
end

...

element :registration_link, "a[href='#{registration_path}']"
element :forgotten_password_link, "a[href='#{forgotten_password_path}']"

Test error:

Failure/Error: let(:forgotten_password_page) { Pages::Players::ForgottenPassword.new }

NameError:
  uninitialized constant Pages::Players::ForgottenPassword

Create spec/pages/players/forgotten_password.rb:

# frozen_string_literal: true

module Pages
  module Players
    class ForgottenPassword < SitePrism::Page
    end
  end
end

Test error:

Failure/Error: expect(forgotten_password_page).to be_displayed

SitePrism::NoUrlMatcherForPageError:
  SitePrism::NoUrlMatcherForPageError

Add the URL:

class ForgottenPassword < SitePrism::Page
  set_url "/players/password/new"
end

That should be all to get the test passing.

The forgotten password page only has one field, email address, and a submit button. Following the examples from registration, our stories are:

context "when the forgotten password form is filled in correctly" do
  it "sets reset_password_token on the player"

  context "when the player clicks the password reset link in their email" do
    it "allows them to change their email address"
  end
end

context "when the email address is missing" do
  it "shows an error message"
end

context "when the email address does not match a player" do
  it "shows an error message"
end

In the first test, I knew that Devise would have to store a token for the forgotten password functionality to work (as with confirmation) so I checked config/schema.rb looking for something that made sense.

The first test will be very similar to it "creates a new, unconfirmed player" do in registration. For this test to work, however, we are going to need to have a confirmed player in the database (you can’t reset the password for a player that doesn’t exist). A confirmed player is something we are going to need regularly throughout our testing. The majority of the application simply won’t work without being signed in as a confirmed player. Given this, it is going to make our lives simplier if there is a player variable that we can call in all of our tests. There are multiple ways to do this but I’m a fan of the factory_bot gem.

factory_bot gets added to your development and test group in the Gemfile:

group :development, :test do
  # Call 'byebug' anywhere in the code to stop execution and get a debugger console
  gem "byebug", platforms: %i[mri mingw x64_mingw]
  gem "factory_bot_rails", "~> 6.1.0"
  gem "rspec-rails", "~> 4.0.1"
end

Make sure you install:

bundle

Now factory_bot needs to be loaded into RSpec. The factory_bot documentation recommends creating a spec/support directory and then adding the file spec/support/factory_bot.rb:

# frozen_string_literal: true

RSpec.configure do |config|
  config.include FactoryBot::Syntax::Methods
end

Then this file needs to be included in spec/rails_helper.rb. Previously we included all the files in spec/pages by adding:

Dir[Rails.root.join("spec/pages/**/*.rb")].sort.each { |f| require f }

Directly above this line should be:

# Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f }

Remove the hash and update to read:

Dir[Rails.root.join("spec/support/**/*.rb")].sort.each { |f| require f }

As always, commit this as a separate story arc:

git add --intent-to-add spec/support
git add --patch Gemfile* spec/rails_helper.rb The send_rese/support
git commit

Install factory_bot_rails

To create your first factory, create the directory spec/factories and then create the file spec/factories/player.rb.

# frozen_string_literal: true

FactoryBot.define do
  factory :player do
  end
end

By default, factory_bot will assume that the name of your factory is also the name of the model. In our tests, if we write create(:player), factory_bot will create a new player in the database. build(:player) will give you a new player but that player will not yet be stored in the database. Inside the factory block, we can set the defaults we want to use:

FactoryBot.define do
  factory :player do
    email { "player@example.com" }
    password { "Passw0rd" }
    confirmed_at { Time.zone.now }
  end
end

By setting the confirmed_at date, the player will be confirmed when we create it.

Now we can create the test:

context "when the forgotten password form is filled in correctly" do
  let(:player) { create(:player) }

  it "sets reset_password_token on the player" do
    forgotten_password_page.load

    forgotten_password_page.email_field.set(player.email)

    expect { forgotten_password_page.reset_password_button.click }.
      to change { player.reload.reset_password_token }
  end

  context "when the player clicks the password reset link in their email" do
    it "allows them to change their email address"
  end
end

Test error:

Failure/Error: forgotten_password_page.email_field.set(player.email)

NoMethodError:
  undefined method `email_field'

Add email_field to Pages::Players::ForgottenPassword exactly the same as in Registration:

class ForgottenPassword < SitePrism::Page
  set_url "/players/password/new"

  element :email_field, "#new_player input[name='player[email]']"
end

Test error:

Failure/Error:
  expect { forgotten_password_page.reset_password_button.click }.
    to change { player.reload.reset_password_token }

NoMethodError:
  undefined method `reset_password_button'

Add the reset_password_button element:

element :email_field, "#new_player input[name='player[email]']"
element :reset_password_button, "#new_player input[type=submit]"

Assuming the test is now green, we can move onto the unhappy paths. These are nothing more than modified versions of the registration tests:

context "when the email address is missing" do
  it "shows an error message" do
    forgotten_password_page.load

    forgotten_password_page.reset_password_button.click

    expect(forgotten_password_page).to have_error_container
  end
end

context "when the email address does not match a player" do
  it "shows an error message" do
    forgotten_password_page.load

    forgotten_password_page.email_field.set("invalid@example.com")
    forgotten_password_page.reset_password_button.click

    expect(forgotten_password_page).to have_error_container
  end
end

Test error:

Failure/Error: expect(forgotten_password_page).to have_error_container

NoMethodError:
  undefined method `has_error_container?'

Add error_container:

element :reset_password_button, "#new_player input[type=submit]"

element :error_container, "#error_explanation"

The final test for this section is:

context "when the player clicks the password reset link in their email" do
  it "allows them to change their email address" do
    token = player.send_reset_password_instructions

    reset_password_page = Pages::Players::ResetPassword.new
    reset_password_page.load(token: token)

    reset_password_page.password_field.set("Password_2")
    reset_password_page.password_confirmation_field.set("Password_2")

    expect { reset_password_page.reset_password_button.click }.
      to change { player.reload.encrypted_password }
  end
end

The method send_reset_password_instructions will create the reset_password_token on the player. Devise encrypts the token and so the token stored to reset_password_token is different from the one used in the URL. I only found this out from many failures over the years and a lot of Googling. I found send_reset_password_instructions in the Devise test files.

Test error:

Failure/Error: reset_password_page = Pages::Players::ResetPassword.new

NameError:
  uninitialized constant Pages::Players::ResetPassword

Create spec/pages/players/reset_password.rb:

# frozen_string_literal: true

module Pages
  module Players
    class ResetPassword < SitePrism::Page
    end
  end
end

Test error:

Failure/Error: reset_password_page.load(token: "token")

SitePrism::NoUrlForPageError:
  SitePrism::NoUrlForPageError

Based on the table of routes, we assumed that the edit action would be the most likely place for the link to take us. Checking the controller in the Devise code, we see the comment:

# GET /resource/password/edit?reset_password_token=abcdef

which leads us to conclude that the url should be set as:

class ResetPassword < SitePrism::Page
  set_url "/players/password/edit?reset_password_token={token}"
end

Test error:

Failure/Error: reset_password_page.password_field.set("Password_2")

NoMethodError:
  undefined method `password_field'
set_url "/players/password/edit?reset_password_token={token}"

element :password_field, "#new_player input[name='player[password]']"

Test error:

Failure/Error: reset_password_page.password_confirmation_field.set("Password_2")

NoMethodError:
  undefined method `password_confirmation_field'
element :password_field, "#new_player input[name='player[password]']"
element :password_confirmation_field,
        "#new_player input[name='player[password_confirmation]']"

Test error:

Failure/Error:
  expect { reset_password_page.reset_password_button.click }.
    to change { player.reload.encrypted_password }

NoMethodError:
  undefined method `reset_password_button'
element :password_field, "#new_player input[name='player[password]']"
element :password_confirmation_field,
        "#new_player input[name='player[password_confirmation]']"
element :reset_password_button, "#new_player input[type=submit]"

You should now be green and can commit again:

rubocop
rspec
git add --intent-to-add spec
git add --patch
git commit

Add tests for player forgotten password

That’s all the testing we are going to do for now. My code at this point is available on GitHub.

Next week we’ll hang up the testing hat and start making pretty things.