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

Posted

技术标签:

【中文标题】Redux 使用 jest、nock、axios 和 jsdom 测试 multipart/form-data【英文标题】:Redux testing multipart/form-data with jest, nock, axios and jsdom 【发布时间】:2018-03-30 20:00:38 【问题描述】:

我正在使用 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 对象以使该测试正常工作?

【问题讨论】:

你发现了吗?我正在尝试做类似的事情,但我得到了FormData is not a constructor。还需要以某种方式模拟 FormData。 不幸的是我没有找到解决这个问题的方法。所以我能做的就是评论这个测试。 【参考方案1】:

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

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

你有几个选择:

1。最简单的解决方案

只是不要匹配PUT 请求中的数据。您嘲笑的原因是因为您不希望发出真正的 HTTP 请求,但希望返回虚假的响应。 nock 只模拟一次请求,所以如果你模拟所有PUT/user/photo 的请求,nock 会捕获它,但仅用于该测试:

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

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

2。在不匹配请求时让 nock 失败的简单解决方案

如果您的代码通过了损坏或错误的文件,您确实希望测试失败,那么最简单的解决方案是测试文件名。由于你的文件是空的,所以不需要匹配内容,但我们可以匹配文件名:

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。匹配表单数据字段的内容

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

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)
      )
  )

【讨论】:

【参考方案2】:

我正在处理同样的问题,问题是 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 标头

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

【讨论】:

以上是关于Redux 使用 jest、nock、axios 和 jsdom 测试 multipart/form-data的主要内容,如果未能解决你的问题,请参考以下文章

使用 JEST 和 nock 进行测试导致“禁止跨源 null”

Nock 在 Redux 测试中没有拦截 API 调用

如何用 jest 测试 redux saga?

在未弹出 Redux 的情况下测试 Jest React-Native Expo CRNA

使用 Detox 和 Nock 模拟 API 调用

错误:在带有 nock 和 mock-store 的 Redux 应用程序的异步操作测试中,操作可能未定义