That’s it. Do what it says on the tin.

All joking aside, surprising though it may be, this is honestly one of the most valuable skills you’ll learn with automated testing. Most testing tools these days offer excellent support and feedback to the developer when a test fails. Couple this technique with call your shots, and you’re well on your way to a really good time. This technique can also be enhanced and more user-friendly if you force your testing tool to give you better error messages.

Generally, I find this is one of the bigger hurdles for people learning test driven development. Folks aren’t accustomed to their tools giving them adequate feedback to get the work done. Or in their excitment, they rush to over-coding before the system has guided them to.

Let’s look at some basic examples.

Examples

Javascript/React

Imagine we’re building a very simple Button component. We might write a test like this:

test('calls the passed onClick function onClick', async () => {
  const onClick = vi.fn();
  const { getByRole } = render(<BadButton onClick={onClick}>Button!</BadButton>);

  expect(onClick).not.toHaveBeenCalled();

  await getByRole('button', { name: 'Button!' }).click();

  await expect(onClick).toHaveBeenCalled();
});

Then, we implement it.

function BadButton({ onClick, children }) {
  return <div onClick={onClick}>{children}</div>;
}

The eagle-eyed among you will have noticed the bug. But a tired developer may not. Or worse, they’ll see the failure, “know” they got it right, and delete the test. Or spend unnecessary cycles wondering why it failed.

Instead, they could read the error.

TestingLibraryElementError: Unable to find an accessible element with the role "button" and name "Button!"

There are no accessible roles. But there might be some inaccessible roles. If you wish to access them, then set the `hidden` option to `true`. Learn more about this here: https://testing-library.com/docs/dom-testing-library/api-queries#byrole

Now, I’ll say, this is a decent error message. If you’re familiar with the web and the general tool ecosystem here, this will almost certainly make you facepalm. You spend two seconds reading the error, fix the tiny bug, and go on your way.

If you don’t know the platform, don’t worry, the fix is subtle, but quick and easy to achieve. We simply replace the div tag (a non-interactive element), with a button tag, and we’re off to the races.

// Corrected
function BadButton({ onClick, children }) {
  return <button onClick={onClick}>{children}</button>;
}

Ruby/Rails

One of my favorite tasks for teaching this particular skill is test-driving a Ruby on Rails controller/API endpoint. It’s almost kata-like.

Let’s start outside-in, with an “acceptance test,” and see how the error messages guide the experience. Note: I’m using RSpec here, but the same general approach works with MiniTest.

RSpec.describe UsersController do
  it "returns 201 created" do
    post :create, params: {
      username: "dugancathal",
      password: "Password1!",
    }

    expect(response).to have_http_status(:created)
  end
end

Unsurprisingly, the first error is NameError: uninitialized constant UsersController. Let’s make one of those.

class UsersController < ApplicationController
end

The error message then says:

ActionController::UrlGenerationError:
  No route matches {action: "create", controller: "users"}

That’s pretty self explanatory, the route doesn’t exist. Moving right along, we create that.

Rails.application.routes.draw do
  resources :users, only: [:create]
end

Probably my favorite error message in this experience. This isn’t quite as good as Rust’s famous compiler errors, but it’s pretty close.

AbstractController::ActionNotFound:
  The action 'create' could not be found for UsersController

Sounds like we need to make an action on the UsersController.

class UsersController < ApplicationController
  def create
  end
end

Ok, granted, this one’s a little opaque, but it kinda makes sense once you think about it for a second. The error says we expected a 201 status code, but got a 204 (no content). What are we rendering in our new action? Nothing! So of course, Rails does the helpful thing and says, “I’ll just return a 204 for you.”

Failure/Error: expect(response).to have_http_status(:created)
  expected the response to have status code :created (201) but it was :no_content (204)

That said, that’s not what we want. Instead, we want a 201. The simplest thing I can do to make that pass?

def create
  render status: :created, plain: ""
end

You’ll have to forgive that dangling plain: ''. It’s an artifact of the rendering cycle of Rails controllers, which is outside the scope of this article.

But, as you can see, at every step along the way, our test error messages gave us strong feedback and clues as to our next step. Imagine combining this level of feedback with the [call the shots] habit! Once you’ve done this a few times, you’ll have a firm grasp on the errors you should expect and know exactly when/if you go off track.

Conclusion

It may seem a silly thing, and sillier still, having to state it out loud. But reading your error messages is an incredibly valuable technique and skill. It improves your awareness of the state of the system and speeds up your feedback loops. The diagnostics your app provides are powerful - leverage them!

The next time a test fails, resist the urge to immediately dive in and change something. Stop, think for a second, and let it tell you what to do next. It doesn’t give you enough information? Maybe try getting better error messages.