Pragmatic Testing in AdonisJS with Japa #5.2

Testing with Lucid Model Factories

In This Lesson

We'll learn about AdonisJS Model Factories and how we can use them to create realistic, yet fake, test data. We'll create a few factories we'll use in this series and learn how we can stub or create data with them in tests.

Created by
@tomgobich
Published

In the last lesson, we manually defined our users' properties, their full name, email, and password. That's ok for the small amount of data needed for our users, but this can get very excessive with larger models. To help with this, we can introduce factories.

Factories allow us to create real model instances and rows within our database using fake data. This will allow us to create a user by simply calling:

const user = await UserFactory.create();
Copied!

This substantially cleans things up in the overall picture of our tests. So, let's start by creating a couple of factories.

# this is for our user
node ace make:factory user
Copied!
# this tracks a history of user email changes
node ace make:factory email_history
Copied!
# a simple post we'll use for a demo later on
node ace make:factory post
Copied!
# this is a token used to allow a user to rest their password
node ac make:factory password_reset_token
Copied!

Next, we just need to map our non-relationship-based user properties to faker methods.

export const UserFactory = factory
  .define(User, async ({ faker }) => {
    return {
      fullName: faker.person.fullName(),
      // our registration normalizes email to lowercase, so we'll want to do the same here
      email: faker.internet.email().toLowerCase(),
      password: "something",
    };
  })
  .build();
Copied!

Now, when we use our UserFactory to create a user, it'll call this function to populate a user with a fake name, email, and 'something' as our password. We are hard-coding our password here so that we know what it is in case we ever need to log in for the user. We can also use the merge property on the factory to change this on an individual basis.

Lastly, we can map this via a relationship to our EmailHistoryFactory

export const UserFactory = factory
  .define(User, async ({ faker }) => {
    return {
      fullName: faker.person.fullName(),
      email: faker.internet.email().toLowerCase(),
      password: "something",
    };
  })
  .relation("emailHistories", () => EmailHistoryFactory)
  .build();
Copied!

This will allow us to create a user with pre-existing email history data, if needed. Great, let's next switch over to that factory and fill it out.

export const EmailHistoryFactory = factory
  .define(EmailHistory, async ({ faker }) => {
    return {
      emailOld: faker.internet.email(),
      emailNew: faker.internet.email(),
    };
  })
  .relation("user", () => UserFactory)
  .build();
Copied!

Then, we have our post factory.

export const PostFactory = factory
  .define(Post, async ({ faker }) => {
    return {
      title: faker.book.title(),
      summary: faker.lorem.sentence(4),
      body: faker.lorem.paragraph(),
    };
  })
  .relation("user", () => UserFactory)
  .build();
Copied!

Lastly, let's quickly fill out our password reset factory.

export const PasswordResetTokenFactory = factory
  .define(PasswordResetToken, async ({ faker }) => {
    return {
      value: PasswordResetToken.generate().value,
      expiresAt: DateTime.fromJSDate(faker.date.soon()),
    };
  })
  .state("invalid", (token, ctx) => {
    token.expiresAt = DateTime.fromJSDate(ctx.faker.date.past());
  })
  .relation("user", () => UserFactory)
  .build();
Copied!

The invalid state here will allow us to quickly make an invalid password reset token with the expiry set in the past.

Next, jumping back into our auth.spec.ts we can swap our hard-coded properties with our UserFactory

In our first test, we don't want to actually persist the user into the database, so we'll use UserFactory.makeStubbed(). This will create and return an instance of the model with faker data, but will not persist it to the database. Meaning, we can use the faker data to send with our request.

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

  const user = await UserFactory.makeStubbed();
  const response = await client
    .post(route("auth.register.store"))
    .withCsrfToken()
    .form({
      fullName: user.fullName,
      email: user.email,
      password: "something",
    });

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

  // we can now directly query our database to assert our user was actually created
  const dbUser = await User.findByOrFail("email", user.email);
  assert.equal(dbUser.email, user.email.toLowerCase());

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

In our second test, we want our user to already exist in our database, so we can use UserFactory.create() which will create and return an instance of the model with faker data and persist that model to the database.

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

  const user = await UserFactory.create();

  // const user = await UserFactory.makeStubbed()
  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!