Pragmatic Testing in AdonisJS with Japa #5.1

Testing Database-Driven Endpoints

In This Lesson

We'll learn how to functionally test a registration endpoint using a test database. We'll learn about global transactions and how we can use them to easily reset our database between tests and confirm database state with Lucid.

Created by
@tomgobich
Published

Now that we have a separate database for our tests, we can send API requests to our app, and our app will work within our siloed test database.

Our registration flow will be perfect for these tests, and we've worked with a portion of it before in our fakes lesson when we verified that one of its methods sent a welcome email.

node ace make:test auth --suite=functional
Copied!

Cool, we'll test that our endpoint should "allow a user to register," and we know that this endpoint will attempt to send an email, so we'll want to fake our email service. We can also confirm that the email has been sent.

test.group("Auth", () => {
  test("allow a user to register", async ({ client, route, cleanup }) => {
    const { mails } = mail.fake();
    cleanup(() => mail.restore());

    mails.assertSent(WelcomeEmailNotification, ({ message }) => {
      return message.hasTo(user.email);
    });
  });
});
Copied!

Great, next, let's send our POST request to our register route and make some basic assertions.

test.group("Auth", () => {
  test("allow a user to register", async ({ client, route, cleanup }) => {
    const { mails } = mail.fake();
    cleanup(() => mail.restore());

    const response = await client
      .post(route("auth.register.store"))
      .withCsrfToken()
      .form({
        fullName: "Phillip J Fry",
        email: "Test@test.com",
        password: "something",
      });

    response.assertOk();
    response.assertRedirectsTo(route("jumpstart"));

    mails.assertSent(WelcomeEmailNotification, ({ message }) => {
      return message.hasTo(user.email.toLowerCase());
    });
  });
});
Copied!

Next, we want to confirm that a user has actually been added to our database. For that, we can use our Lucid Models to query and assert.

test.group("Auth", () => {
  test("allow a user to register", async ({
    assert,
    client,
    route,
    cleanup,
  }) => {
    const { mails } = mail.fake();
    cleanup(() => mail.restore());

    const response = await client
      .post(route("auth.register.store"))
      .withCsrfToken()
      .form({
        fullName: "Phillip J Fry",
        email: "Test@test.com",
        password: "something",
      });

    response.assertOk();
    response.assertRedirectsTo(route("jumpstart"));

    const dbUser = await User.firstOrFail();
    assert.equal(dbUser.email, "test@test.com");

    mails.assertSent(WelcomeEmailNotification, ({ message }) => {
      return message.hasTo(user.email.toLowerCase());
    });
  });
});
Copied!

Now, our database is resetting between our test runs, but not between each test. So, if we were to add another test, our test@test.com would already exist in that test. It's best practice not to allow side effects to persist between tests. This will ensure we don't have to worry about working with unique emails and the like between all our tests.

For this, we can use our test group's each hook and our db testUtils again. If we aren't using transactions within our endpoints, then we can use a global transaction via:

test.group("Auth", (group) => {
  group.each.setup(() => testUtils.db().withGlobalTransaction());

  // ...
});
Copied!

However, transactions can't be nested, so if we are using global transactions elsewhere, then we'll want to use truncation.

test.group("Auth", (group) => {
  group.each.setup(() => testUtils.db().truncate());

  // ...
});
Copied!

We are not, at least not in authentication, so we're safe to use the transaction.

Let's add another test to ensure an email that already exists in our database can't register again.

test("fail to register user when email is already taken", async ({
  client,
  route,
  cleanup,
}) => {
  const { mails } = mail.fake();
  cleanup(() => mail.restore());

  const user = await User.create({
    fullName: "Test User",
    email: "test@test.com",
    password: "something",
  });

  mails.assertNotSent(WelcomeEmailNotification);
});
Copied!

First, we want to create a user so that a user already exists within our database for this test. Then, we can render our request and assert that the user should be redirected back to the defined referer, and we get our email validation rule flashed.

test("fail to register user when email is already taken", async ({
  client,
  route,
  cleanup,
}) => {
  const { mails } = mail.fake();
  cleanup(() => mail.restore());

  const user = await User.create({
    fullName: "Test User",
    email: "test@test.com",
    password: "something",
  });

  const response = await client
    .post(route("auth.register.store"))
    .withCsrfToken()
    .header("Referer", route("auth.register.show"))
    .form({
      fullName: user.fullName,
      email: user.email,
      password: "something",
    })
    .redirects(0);

  response.assertHeader("Location", route("auth.register.show"));
  response.assertFlashMessage("errors.email", [
    "The email has already been taken",
  ]);

  mails.assertNotSent(WelcomeEmailNotification);
});
Copied!

Join the Discussion 0 comments

Create a free account to join in on the discussion
robot comment bubble

Be the first to comment!