Pragmatic Testing in AdonisJS with Japa #4.6

Testing File Uploads

In This Lesson

We'll learn how to test AdonisJS file uploads using Drive Fakes and the Japa API Client. We'll generate fake files for testing, use a multipart request, assert successes, test validation failures, and impress the importance of cleanup.

Created by
@tomgobich
Published

Notes Used to Craft this Lesson

The native file upload option for AdonisJS is to use Drive; outside of AdonisJS, this package is called Flydrive. Like Japa can be used outside of AdonisJS projects, Flydrive is the same. Also, similar to mail and the other first-party AdonisJS packages, Drive comes with fakes and helpful assertions when it comes to testing.

The project we're working on already has Drive installed and configured, and our PostsController contains a simple uploadThumbnail method which we'll be testing here today.

export default class PostsController {
  // ...

  async uploadThumbnail({ request, response }: HttpContext) {
    const image = request.file('thumbnail', {
      size: '5mb',
      extnames: ['jpeg', 'jpg', 'png'],
    })

    if (!image) {
      return response.badRequest({
        error: 'Image not provided',
      })
    }

    if (image.hasErrors) {
      return response.unprocessableEntity(image.errors)
    }

    const key = `${cuid()}.${image.extname}`
    await image.moveToDisk(key)

    return {
      message: 'Image successfully uploaded',
      filename: key,
      url: image.meta.url,
    }
  }

  // ...
}
Copied!
  • app
  • controllers
  • posts_controller.ts

To aid with testing further, let's add a utility package@poppinss/file-generator, that will allow us to generate files for testing purposes.

npm i -D @poppinss/file-generator
Copied!

Great, let's get a test added. Since we're testing a method within our PostsController, we can keep this in posts.spec.ts.

test("allow a valid thumbnail to be uploaded", async ({
  assert,
  client,
  route,
  cleanup,
}) => {
  const disk = drive.fake();
  cleanup(() => drive.restore());
});
Copied!

Next, let's use that file generator package to create a file to work with. We want this to test the happy path, so we'll keep it within our extension names and file size restrictions.

test("allow a valid thumbnail to be uploaded", async ({
  assert,
  client,
  route,
  cleanup,
}) => {
  const disk = drive.fake();
  cleanup(() => drive.restore());

  const file = await fileGenerator.generatePng("3mb");
});
Copied!

Next, we can use the client to send a POST request to our posts.thumbnail route. We can use file on our client to mimic a multipart request containing a file. The first argument is the field name, the second is the file contents, and the third is the options.

test("allow a valid thumbnail to be uploaded", async ({
  assert,
  client,
  route,
  cleanup,
}) => {
  const disk = drive.fake();
  cleanup(() => drive.restore());

  const file = await fileGenerator.generatePng("3mb");

  const response = await client
    .post(route("posts.thumbnail"))
    .withCsrfToken()
    .file("thumbnail", file.contents, {
      filename: file.name,
      contentType: file.mime,
    });
});
Copied!

We can make some assertions against our response now. We'll start at the surface and work our way deeper.

test("allow a valid thumbnail to be uploaded", async ({
  assert,
  client,
  route,
  cleanup,
}) => {
  const disk = drive.fake();
  cleanup(() => drive.restore());

  const file = await fileGenerator.generatePng("3mb");

  const response = await client
    .post(route("posts.thumbnail"))
    .withCsrfToken()
    .file("thumbnail", file.contents, {
      filename: file.name,
      contentType: file.mime,
    });

  const body = response.body();

  response.assertOk();
  response.assertBodyContains({
    message: "Image successfully uploaded",
  });

  assert.properties(body, ["url", "filename"]);
});
Copied!

Finally, we can use our fake Drive to confirm the file exists

test("allow a valid thumbnail to be uploaded", async ({
  assert,
  client,
  route,
  cleanup,
}) => {
  const disk = drive.fake();
  cleanup(() => drive.restore());

  const file = await fileGenerator.generatePng("3mb");

  const response = await client
    .post(route("posts.thumbnail"))
    .withCsrfToken()
    .file("thumbnail", file.contents, {
      filename: file.name,
      contentType: file.mime,
    });

  const body = response.body();

  response.assertOk();
  response.assertBodyContains({
    message: "Image successfully uploaded",
  });

  assert.properties(body, ["url", "filename"]);
  disk.assertExists(body.filename);
});
Copied!

Okay, let's pause for a second. First, if you need to fake a specific disk with Drive, that goes into drive.fake() and drive.response() methods as the first argument, ex: drive.fake('fs').

Second, this is a prime spot to observe why we want to clean up our fakes, because our Drive fake will actually store the file in our project until we restore the fake. So, if we comment out our cleanup, we can actually see the file that we've uploaded within our tmp folder.

So, do yourself a favor and always restore your fakes!

Great, so next, we have our sad paths. First, let's ensure our file validations are working. We'll go ahead and test our file size and type in one go by sending a PDF over 5MB.

test("does not allow an invalid thumbnail to be uploaded", async ({
  client,
  route,
  cleanup,
}) => {
  drive.fake();
  cleanup(() => drive.restore());

  const file = await fileGenerator.generatePdf("6mb");

  const response = await client
    .post(route("posts.thumbnail"))
    .withCsrfToken()
    .file("thumbnail", file.contents, {
      filename: file.name,
      contentType: file.mime,
    });

  response.assertUnprocessableEntity();
  response.assertBodyContains([
    {
      fieldName: "thumbnail",
      type: "size",
      message: "File size should be less than 5MB",
    },
    {
      fieldName: "thumbnail",
      type: "extname",
      message: "Invalid file extension pdf. Only jpeg, jpg, png are allowed",
    },
  ]);
});
Copied!

Perfect! Then, we also want to test that our controller gracefully handles if the file is missing or misnamed.

test("gracefully handle when an uploaded file is missing/misnamed", async ({
  client,
  route,
  cleanup,
}) => {
  drive.fake();
  cleanup(() => drive.restore());

  const file = await fileGenerator.generatePng("6mb");

  const response = await client
    .post(route("posts.thumbnail"))
    .withCsrfToken()
    .file("thumbnails", file.contents, {
      filename: file.name,
      contentType: file.mime,
    });

  response.assertBadRequest();
  response.assertBody({ error: "Image not provided" });
});
Copied!

Join the Discussion 0 comments

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

Be the first to comment!