Redux使用jest,nock,axios和jsdom测试multipart / form-data

问题描述 投票:2回答:1

我正在使用jest + nock + jsdom模块来测试我的React \ Redux应用程序。我需要测试这个异步动作函数:

export function updateUserPhoto (file, token) {
  const data = new FormData()
  data.append('file', file)

  return dispatch => {
    dispatch(userPutPhotoRequest())
    return axios({
      method: 'PUT',
      headers: {
        'x-access-token': token
      },
      data: data,
      url: API_URL + '/user/photo'
    })
      .then(res => dispatch(userPutPhotoSuccess(res.data)))
      .catch(err => dispatch(userPutPhotoFilure(err)))
  }
}

所以我使用jsdom将FormData和File对象提供给测试:

const {JSDOM} = require('jsdom')

const jsdom = (new JSDOM(''))
global.window = jsdom.window
global.document = jsdom.window.document
global.FormData = jsdom.window.FormData
const File = jsdom.window.File
global.File = jsdom.window.File

这是测试“上传照片”功能的方法:

it('creates USER_UPDATE_SUCCESS when updating user photo has been done', () => {
    const store = mockStore(Map())

    const file = new File([''], 'filename.txt', {
      type: 'text/plain',
      lastModified: new Date()
    })

    const expectedFormData = new FormData()
    expectedFormData.append('file', file)

    nock(API_URL, {
      reqheaders: {
        'x-access-token': token
      }
    }).put('/user/photo', expectedFormData)
      .reply(200, {body: {}})

    const expectedActions = [
      {
        type: ActionTypes.USER_PUT_PHOTO_REQUEST
      },
      {
        type: ActionTypes.USER_PUT_PHOTO_SUCCESS,
        response: {
          body: {}
        }
      }
    ]

    return store.dispatch(actions.updateUserPhoto(file, token))
      .then(() => {
        // return of async actions
        expect(store.getActions()).toEqual(expectedActions)
      })
  })

我正在使用nock来模拟axios请求,redux-mock-store来模拟Redux商店。创建File和FormData对象以将其与axios的响应进行比较。然后我调用动作函数传递文件和令牌作为参数。

在生产动作功能工作和调度动作成功。但在测试中我收到错误:

Error: Data after transformation must be a string, an ArrayBuffer, a Buffer, or a Stream

当我在数据测试通过时传入axios空对象时,FormData对象出现问题。如何以适当的方式模拟Axios的FormData对象以使此测试工作?

reactjs redux jestjs axios form-data
1个回答
1
投票

这个答案来得太迟了,但我想做类似的事情,我想在这里发布一个其他人可能偶然发现并找到有用的解决方案。

这里的主要问题是nock模拟网络请求而不是Javascript库。 FormData是一个Javascript对象,最终在发出网络请求时会转换为文本。当FormData对象使它变为nock时,它被转换为stringBuffer,因此你看到的错误。 nock无法使用FormData对象进行比较。

你有几个选择:

1. Easiest solution

只是不要与PUT请求中的数据匹配。你嘲笑的原因是因为你不想要一个真正的HTTP请求,但你想要一个假响应。 nock只嘲笑一次请求,所以如果你模拟所有PUT请求/user/photo nock将捕获它但仅用于该测试:

nock(API_URL, {
  reqheaders: {
    'x-access-token': token
  }
}).put('/user/photo')
  .reply(200, {body: {}})

在以这种方式实施测试之前,请考虑您的测试要验证的内容。您是否尝试验证该文件是否在HTTP请求中发送?如果是,那么这是一个糟糕的选择。您的代码可以发送与调度的文件完全不同的文件,但仍然通过此测试。但是,如果您有另一个测试来验证文件是否正确放入HTTP请求,那么此解决方案可能会为您节省一些时间。

2. Easy solution for getting nock to fail on not matching the request

如果您希望测试失败,如果您的代码传递了损坏或错误的文件,那么他最简单的解决方案是测试文件名。由于您的文件为空,因此无需匹配内容,但我们可以匹配文件名:

nock(API_URL, {
  reqheaders: {
    'x-access-token': token
  }
}).put('/user/photo', /Content-Disposition\s*:\s*form-data\s*;\s*name="file"\s*;\s*filename="filename.txt"/i)
  .reply(200, {body: {}})

这应该与您上传一个文件的简单情况相匹配。

3. Matching the content of form data fields

假设您有其他字段要添加到您的请求中

export function updateUserPhoto (file, tags, token) {
  const data = new FormData()
  data.append('file', file)
  data.append('tags', tags)
  ...

或者您要在文件中包含要匹配的虚假内容

const file = new File(Array.from('file contents'), 'filename.txt', {
  type: 'text/plain',
  lastModified: new Date()
})

这是事情变得有点复杂的地方。基本上,您需要做的是将表单数据文本解析回对象,然后编写自己的匹配逻辑。

parse-multipart-data是一个相当简单的解析器,你可以使用:

https://www.npmjs.com/package/parse-multipart-data

使用该包,您的测试可能看起来像这样

it('creates USER_UPDATE_SUCCESS when updating user photo has been done', () => {
    const store = mockStore(Map())

    const file = new File(Array.from('file content'), 'filename.txt', {
      type: 'text/plain',
      lastModified: new Date()
    })

    nock(API_URL, {
      reqheaders: {
        'x-access-token': token
      }
    }).put('/user/photo', function (body) { /* You cannot use a fat-arrow function since we need to access the request headers */
        // Multipart Data has a 'boundary' that works as a delimiter.
        // You need to extract that
        const boundary = this.headers['content-disposition']
          .match(/boundary="([^"]+)"/)[1];

        const parts = multipart.Parse(Buffer.from(body),boundary);

        // return true to indicate a match
        return parts[0].filename === 'filename.txt'
          && parts[0].type === 'text/plain'
          && parts[0].data.toString('utf8') === 'file contents'
          && parts[1].name === 'tags[]'
          && parts[1].data.toString('utf8') === 'tag1'
          && parts[2].name === 'tags[]'
          && parts[2].data.toString('utf8') === 'tag2';
      })
      .reply(200, {body: {}})

    const expectedActions = [
      {
        type: ActionTypes.USER_PUT_PHOTO_REQUEST
      },
      {
        type: ActionTypes.USER_PUT_PHOTO_SUCCESS,
        response: {
          body: {}
        }
      }
    ]

    return store.dispatch(actions.updateUserPhoto(file, ['tag1', 'tag2'], token))
      .then(() => {
        // return of async actions
        expect(store.getActions()).toEqual(expectedActions)
      })
  })

0
投票

我正在处理同样的问题,问题是axios将http设置为默认适配器。而xhr是你需要的。

// axios/lib/defaults.js
function getDefaultAdapter() {
  var adapter;
  // Only Node.JS has a process variable that is of [[Class]] process
  if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
    // For node use HTTP adapter
    adapter = require('./adapters/http');
  } else if (typeof XMLHttpRequest !== 'undefined') {
    // For browsers use XHR adapter
    adapter = require('./adapters/xhr');
  }
  return adapter;
}

因此,在axios调用上明确设置xhr适配器对我有用。

就像是:

export function updateUserPhoto (file, token) {
  const data = new FormData()
  data.append('file', file)

  return dispatch => {
    dispatch(userPutPhotoRequest())
    return axios({
      method: 'PUT',
      headers: {
        'x-access-token': token
      },
      adapter: require('axios/lib/adapters/xhr'),
      data: data,
      url: API_URL + '/user/photo'
    })
      .then(res => dispatch(userPutPhotoSuccess(res.data)))
      .catch(err => dispatch(userPutPhotoFilure(err)))
  }
}

另外,我遇​​到了nock和CORS的问题,所以,如果你遇到同样的问题,你可以添加access-control-allow-origin header

nock(API_URL, {
  reqheaders: {
    'x-access-token': token
  }
})
.defaultReplyHeaders({ 'access-control-allow-origin': '*' })
.put('/user/photo', expectedFormData)
.reply(200, {body: {}})

© www.soinside.com 2019 - 2024. All rights reserved.