Mocking File Upload in Vue with Jest

Written By Rachel Opperman
Posted on

Testing your code is important, but it can also be time-consuming. In my experience, when it comes to testing Vue with Jest, half the battle is figuring out how to mock something. Sometimes, I can spend 1-hour building a feature and 2+ hours writing tests for that feature. Mocking file upload is a perfect example of that, although it admittedly took me much longer than 2-hours to finally figure it out.

Vue Test Utils is a great package for Vue testing, and it provides many helper methods for mocking the functionality of different HTML form elements, but an input of type file is unfortunately not included. In this post, we’ll discuss how to combine Vue Test Utils helpers with Jest helpers to mock and test file upload in Vue (and hopefully spare you hours of frustration).

Setup

We’ll be focusing on image upload in this example, but the general principles should be applicable to other file types. First, we have a form element with an input of type file:

<form
   method="POST"
   enctype="multipart/form-data"
 >
   <input
     @change="onChange"
     type="file"
     accept="image/*"
   >
 </form>

This form contains a single input that will accept images of any type, and the form will be submitting FormData, as designated by enctype=”multipart/form-data”.

When a file is uploaded, the onChange method will be triggered:

onChange(event) {
  if (!event.target.files.length) { return }
 
  const image = event.target.files[0]
  const reader = new FileReader()
 
  reader.readAsDataURL(image)
 
  this.persist(image)
},
persist(image) {
  const data = new FormData()
 
  data.append('image', image)
 
  // Send the image to the API (e.g., with a Vuex action)
},

In the onChange method, we’re retrieving the image from the event and converting it to a base64 encoded string so that it can be attached as FormData in the persist method. Note that the first argument of data.append() needs to match the property name that your API is expecting.

But How Do We Test This?

To test this with Jest, we’ll need to mock 2 things:

  • The event
  • The FileReader.readAsDataURL() method
// profile-image-form.spec.js
 
import { mount } from '@vue/test-utils'
import ProfileImageForm from '@/components/ProfileImageForm'
 
const event = {
  target: {
    files: [
      {
        name: 'image.png',
        size: 50000,
        type: 'image/png',
      },
    ],
  },
}
 
describe('ProfileImageForm', () => {
  it('makes a call to persist the image on image upload', () => {
    // Mount the component
    const wrapper = mount(ProfileImageForm)
 
    // Mock FileReader.readAsDataURL() to be a function that returns null
    const fileReaderSpy = jest.spyOn(FileReader.prototype, 'readAsDataURL').mockImplementation(() => null)
 
    // Spy on the component’s persist() method
    const persistSpy = jest.spyOn(wrapper.vm, 'persist')
 
    // Manually trigger the component’s onChange() method
    wrapper.vm.onChange(event)
 
    // Assert that the FileReader object was called with the uploaded image
    expect(fileReaderSpy).toHaveBeenCalledWith(event.target.files[0])
 
    // Assert that the component’s persist() method was called with the uploaded image
    expect(persistSpy).toHaveBeenCalledWith(event.target.files[0])
  })
}

What’s Going on Here?

First, we need to create an event object that mimics the structure of an actual image upload event. Next, we need to mount the component using the mount method provided by Vue Test Utils.

Now comes the fun part. We mock FileReader.readAsDataURL() by spying on the FileReader object, specifically upon its prototype because that’s where the object’s methods are located. We then specify the exact method with the ‘readAsDataURL’ argument. If we only provide FileReader as the first argument, we’ll get an error that the readAsDataURL property is not a function.

Simply spying on the method won’t help us, though, because it won’t change how the method works. We also need to mock its implementation, which is what we do with .mockImplementation(() => null). In this case, we’re saying that FileReader.readAsDataURL() should be a method that returns null. We’re having the method return null because we don’t actually need to convert our fake event into a base64 encoded string; we just need to check that the method was called with the uploaded image.

Our other spy (persistSpy) is created so we can assert that the component’s persist method was called.

Finally, all that’s left to do is trigger the component’s onChange method. The method has to be triggered manually because we can’t actually upload a file in our test, and we also don’t need to spend time testing the built-in functionality of the browser. Once the method has been triggered, we can assert that the readAsDataURL and persist methods were called with the uploaded image.

Testing Vue components with Jest can be complex, but it doesn’t have to be. When you come across a built-in method, like FileReader.readAsDataURL(), that requires a very specific input that you can’t provide in a test, all you have to do is mock that method’s implementation. In doing so, you’ll prevent it from trying to do all the things it normally does behind the scenes and be able to test what you really need to test — that the method was called with a specific argument.

If you have other ways of testing Vue file upload, let us know @zaengle. We’d love to hear about them.