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 userCopied!
# this tracks a history of user email changes node ace make:factory email_historyCopied!
# a simple post we'll use for a demo later on node ace make:factory postCopied!
# this is a token used to allow a user to rest their password node ac make:factory password_reset_tokenCopied!
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!