JavaScript測試教程–part 4:模擬 API 調用和模擬 React 組件交互

做者:Marcin Wanagojavascript

翻譯:瘋狂的技術宅html

原文:wanago.io/2018/09/17/…前端

未經容許嚴禁轉載java

今天,咱們進一步測試 React 組件。它涉及模擬組件交互和模擬 API 調用。你將學到兩種方法,開始吧!react

模擬

對於咱們的程序來講,從 API 獲取一些數據是很常見的。可是它可能因爲各類緣由而失敗,例如 API 被關閉。咱們但願測試可靠且獨立,並確保能夠模擬某些模塊。咱們把 ToDoList 組件修改成智能組件。webpack

app/components/ToDoList.component.js
import React, { Component } from 'react';
import Task from "../Task/Task";
import axios from 'axios';
 
class ToDoList extends Component {
  state = {
    tasks: []
  }
  componentDidMount() {
    return axios.get(`${apiUrl}/tasks`)
      .then(tasksResponse => {
        this.setState({
          tasks: tasksResponse.data
        })
      })
      .catch(error => {
        console.log(error);
      })
  }
  render() {
    return (
      <div> <h1>ToDoList</h1> <ul> { this.state.tasks.map(task => <Task key={task.id} id={task.id} name={task.name}/> ) } </ul> </div> ) } } export default ToDoList; 複製代碼

它使用 axios 提取數據,因此須要模擬該模塊,由於咱們不但願發出實際的請求。此類模擬文件在 __ mocks __ 目錄中定義,在該目錄中,文件名被視爲模擬模塊的名稱。ios

__mocks__/axios.js
'use strict';
module.exports = {
  get: () => {
    return Promise.resolve({
      data: [
        {
          id: 0,
          name: 'Wash the dishes'
        },
        {
          id: 1,
          name: 'Make the bed'
        }
      ]
    });
  }
};
複製代碼

若是你要模擬 Node 的某些核心模塊(例如 fspath ),則須要在模擬文件中明確調用 jest.mock('moduleName')web

Jest 容許咱們對函數進行監視:接下來測試是否調用了咱們所建立的 get 函數。json

app/components/ToDoList.test.js
import React from 'react';
import { shallow } from 'enzyme';
import ToDoList from './ToDoList';
import axios from 'axios';
 
jest.mock('axios');
 
describe('ToDoList component', () => {
  describe('when rendered', () => {
    it('should fetch a list of tasks', () => {
      const getSpy = jest.spyOn(axios, 'get');
      const toDoListInstance = shallow(
        <ToDoList/>
      );
      expect(getSpy).toBeCalled();
    });
  });
});
複製代碼

經過調用 jest.mock('axios'),Jest 在的測試和組件中都用咱們的模擬代替了 axiosaxios

spyOn 函數返回一個 mock函數。有關其功能的完整列表,請閱讀文檔。咱們的測試檢查組件在渲染和運行以後是否從模擬中調用 get函數,併成功執行。

PASS  app/components/ToDoList/ToDoList.test.js
  ToDoList component
    when rendered
      ✓ should fetch a list of tasks
複製代碼

若是你在多個測試中監視模擬函數,請記住清除每一個測試之間的模擬調用,例如經過運行 getSpy.mockClear(),不然函數調用的次數將在測試之間保持不變。你還能夠經過在 package.json 文件中添加如下代碼段來使其成爲默認行爲:

"jest": {
  "clearMocks": true
}
複製代碼

模擬獲取 API

另外一個常見狀況是使用 Fetch API。一個竅門是它是附加到 window 對象的全局函數並對其進行模擬,能夠將其附加到 global 對象。首先,讓咱們建立模擬的 fetch 函數。

__mock__/fetch.js
export default function() {
  return Promise.resolve({
    json: () =>
      Promise.resolve([
        {
          id: 0,
          name: 'Wash the dishes'
        },
        {
          id: 1,
          name: 'Make the bed'
        }
      ])
 
  })
}
複製代碼

而後,將其導入 setupTests.js 文件中。

app/setupTests.js
import Adapter from 'enzyme-adapter-react-16';
import { configure } from 'enzyme';
import fetch from './__mocks__/fetch';
 
global.fetch = fetch;
 
configure({adapter: new Adapter()});
複製代碼

注意,你須要在 package.json 中提供指向 setupTests.js 文件的路徑——它在本教程的第二部分中進行了介紹。

如今你能夠在組件中自由使用 fetch 了。

componentDidMount() {
  return fetch(`${apiUrl}/tasks`)
    .then(tasksResponse => tasksResponse.json())
    .then(tasksData => {
      this.setState({
        tasks: tasksData
      })
    })
    .catch(error => {
      console.log(error);
    })
}
複製代碼

設置監視時,請記住將其設置爲 window.fetch

app/components/ToDoList.test.js
describe('ToDoList component', () => {
  describe('when rendered', () => {
    it('should fetch a list of tasks', () => {
      const fetchSpy = jest.spyOn(window, 'fetch');
      const toDoListInstance = shallow(
        <ToDoList/>
      );
      expect(fetchSpy).toBeCalled();
    });
  });
});
複製代碼

模擬 React 組件的交互

在以前的文章中,咱們提到了閱讀組件的狀態或屬性,但這是在實際與之交互時。爲了說明這一點,咱們將增長一個把任務添加到 ToDoList 的功能。

app/components/ToDoList.js
import React, { Component } from 'react';
import Task from "../Task/Task";
import axios from 'axios';
 
class ToDoList extends Component {
  state = {
    tasks: [],
    newTask: '',
  }
  componentDidMount() {
    return axios.get(`${apiUrl}/tasks`)
      .then(taskResponse => {
        this.setState({
          tasks: taskResponse.data
        })
      })
      .catch(error => {
        console.log(error);
      })
  }
  addATask = () => {
    const {
      newTask,
      tasks
    } = this.state;
    if(newTask) {
      return axios.post(`${apiUrl}/tasks`, {
        task: newTask
      })
        .then(taskResponse => {
          const newTasksArray = [ ...tasks ];
          newTasksArray.push(taskResponse.data.task);
          this.setState({
            tasks: newTasksArray,
            newTask: ''
          })
        })
        .catch(error => {
          console.log(error);
        })
    }
  }
  handleInputChange = (event) => {
    this.setState({
      newTask: event.target.value
    })
  }
  render() {
    const {
      newTask
    } = this.state;
    return (
      <div>
        <h1>ToDoList</h1>
        <input onChange={this.handleInputChange} value={newTask}/>
        <button onClick={this.addATask}>Add a task</button>
        <ul>
          {
            this.state.tasks.map(task =>
              <Task key={task.id} id={task.id} name={task.name}/>
            )
          }
        </ul>
      </div>
    )
  }
}
 
export default ToDoList;

複製代碼

如你所見,咱們在此處使用了 axios.post。這意味着咱們須要擴展 axios 模擬。

__mocks__/axios.js
'use strict';
 
let currentId = 2;
 
module.exports = {
  get: (url) =&gt; {
    return Promise.resolve({
      data: [
        {
          id: 0,
          name: 'Wash the dishes'
        },
        {
          id: 1,
          name: 'Make the bed'
        }
      ]
    });
  },
  post: (url, data) {
    return Promise.resolve({
      data: {
        task: {
          name: data.task,
          id: currentId++
        }
      }
    });
  }
};
複製代碼

我介紹 currentId 變量的緣由是想保持ID惟一

首先檢查修改輸入值是否會改變咱們的狀態。

app/components/ToDoList.test.js
import React from 'react';
import { shallow } from 'enzyme';
import ToDoList from './ToDoList';
 
describe('ToDoList component', () => {
  describe('when the value of its input is changed', () => {
    it('its state should be changed', () => {
      const toDoListInstance = shallow(
        <ToDoList/>
      );
 
      const newTask = 'new task name';
      const taskInput = toDoListInstance.find('input');
      taskInput.simulate('change', { target: { value: newTask }});
 
      expect(toDoListInstance.state().newTask).toEqual(newTask);
    });
  });
});
複製代碼

這裏的關鍵是 simulate 函數調用。它是前面提到過的 ShallowWrapper 的功能。咱們用它來模擬事件。第一個參數是事件的類型(因爲在輸入中使用了 onChange,所以在這裏應該用 change),第二個參數是模擬事件對象。

爲了更進一步,讓咱們測試一下用戶單擊按鈕後是否從的組件發送了實際的請求。

import React from 'react';
import { shallow } from 'enzyme';
import ToDoList from './ToDoList';
import axios from 'axios';
 
jest.mock('axios');
 
describe('ToDoList component', () => {
  describe('when the button is clicked with the input filled out', () => {
    it('a post request should be made', () => {
      const toDoListInstance = shallow(
        <ToDoList/>
      );
      const postSpy = jest.spyOn(axios, 'post');
 
      const newTask = 'new task name';
      const taskInput = toDoListInstance.find('input');
      taskInput.simulate('change', { target: { value: newTask }});
 
      const button = toDoListInstance.find('button');
      button.simulate('click');
 
      expect(postSpy).toBeCalled();
    });
  });
});
複製代碼

測試經過了!

如今事情會變得有些棘手。咱們將要測試狀態是否可以隨着的新任務而更新。有趣的是請求是異步的。

import React from 'react';
import { shallow } from 'enzyme';
import ToDoList from './ToDoList';
import axios from 'axios';
 
jest.mock('axios');
 
describe('ToDoList component', () => {
  describe('when the button is clicked with the input filled out, the new task should be added to the state', () => {
    it('a post request should be made', () => {
      const toDoListInstance = shallow(
        <ToDoList/>
      );
      const postSpy = jest.spyOn(axios, 'post');
 
      const newTask = 'new task name';
      const taskInput = toDoListInstance.find('input');
      taskInput.simulate('change', { target: { value: newTask }});
 
      const button = toDoListInstance.find('button');
      button.simulate('click');
 
      const postPromise = postSpy.mock.results.pop().value;
 
      return postPromise.then((postResponse) => {
        const currentState = toDoListInstance.state();
        expect(currentState.tasks.includes((postResponse.data.task))).toBe(true);
      })
    });
  });
});
複製代碼

如你所見,postSpy.mock.resultspost 全部結果的數組函數,經過它咱們能夠獲得返回的 promise:在 value 屬性中可用。

從測試中返回 promise 是可以確保 Jest 等待其解決的一種方法。

總結

在本文中,咱們介紹了模擬模塊,並將其用於僞造 API 調用。因爲沒有發出實際的請求要求,咱們的測試能夠更可靠、更快。除此以外,咱們還在整個 React 組件中模擬了事件,並檢查了它是否產生了預期的結果,例如組件的請求或狀態變化,而且瞭解了監視的概念。

歡迎關注前端公衆號:前端先鋒,免費領取 webpack 全系列教程。

相關文章
相關標籤/搜索