首页 > 解决方案 > 在 React 和 Jest 中测试 Axios 获取请求

问题描述

我是 React 测试的新手,基本上想知道如何模拟来自 Axios 的成功获取请求的行为。

任何帮助将不胜感激,如果您需要我提供更多信息,请告诉我(我只发布了相关代码)。或者,如果有人知道使用 Axios 进行 React 测试的任何有用链接,也将不胜感激!:)

考试

it("Submit button should make a fetch request return a given Github user.", () => {
    render(<FavouriteLanguage />);
    let mock = new MockAdapter(axios);
    userEvent.click(screen.getByText("Submit"));
    const searchGithubUsers = mock.onGet(
        `https://api.github.com/users/thatguy560/repos?per_page=100`,
        { language: "JavaScript" }
    );
    expect(searchGithubUsers).toHaveBeenCalled();
});

代码

import { Component } from "react";
import axios from "axios";
import sortKeysByValue from "sort-keys-by-value";
import Swal from "sweetalert2";
import "./css/FavouriteLanguage.css";

class FavouriteLanguage extends Component {
    constructor(props) {
        super(props);
        this.state = {
            searchedUsername: "",
            programmingLanguagesUsed: "",
        };
    }

    searchGithubUsers = (event) => {
        this.setState({ searchedUsername: event.target.value }); 
    };

    getData = () => {
        let username = this.state.searchedUsername;
        axios 
        .get(`https://api.github.com/users/${username}/repos?per_page=100`)
        .then((res) => {
            let allProgrammingLanguages = res.data
            .map((data) => data.language)
            .filter((lang) => lang !== null);
            this.setState({ programmingLanguagesUsed: allProgrammingLanguages });
            Swal.fire({
                title: `${
                    username[0].toUpperCase() + username.slice(1).toLowerCase()
                }'s Favourite Programming Language Is:`,
                text: `${this.determineFavouriteLanguage()}`,
                width: 625,
            });
        })
        .catch((error) => {
            Swal.fire({
                title: `${this.DisplayErrorInfo(error)}`,
                icon: "error",
            });
        });
    };


    render() {
        return (
            <div className="App">
                <header className="App-header">
                    <div id="wrap">
                        <h2>Find a user's favourite programming language</h2>
                        <p>Please enter a valid Github username:</p>
                        <form onSubmit={this.getData}>
                            <input
                                type="text"
                                placeholder="Github username..."
                                onChange={this.searchGithubUsers}
                            />
                        </form>
                        <button
                            id="Submit"
                            type="button"
                            className="btn"
                            onClick={this.getData}
                        >
                            Submit
                        </button>
                    </div>
                </header>
            </div>
        );
    }
}

export default FavouriteLanguage;

错误

Matcher error: received value must be a mock or spy function

    Received has type:  object
    Received has value: {"abortRequest": [Function abortRequest], "abortRequestOnce": [Function abortRequestOnce], "networkError": [Function networkError], "networkErrorOnce": [Function networkErrorOnce], "passThrough": [Function passThrough], "reply": [Function reply], "replyOnce": [Function replyOnce], "timeout": [Function timeout], "timeoutOnce": [Function timeoutOnce]}

标签: reactjsunit-testingaxiosjestjsreact-testing-library

解决方案


You're on right track! I'd recommend moving your MockAdapter into its own file (for reusability):

utils/mockAxios.js

import MockAdapter from "axios-mock-adapter";
import axios from "axios";

const mockAxios = new MockAdapter(axios);

export default mockAxios;

Then you can import it, add some mocks to it (you can chain axios responses with replyOnce and/or reply to test both failing/successful API responses, see chaining is also supported), and then make assertions against the result (in this case, spy on Swal.fire and make assertions on how its being called):

__tests__/FavouriteLanguage.js

import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import Swal from "sweetalert2";
import FavouriteLanguage from "./FavouriteLanguage";
import mockAxios from "./utils/mockAxios";

// spying on Swal.fire, turning into a "spy" mocked function
const mockSpy = jest.spyOn(Swal, "fire"); 

const username = "example";
const API_URL = `https://api.github.com/users/${username}/repos?per_page=100`;

// chaining axios responses...
mockAxios
  .onGet(API_URL)
  .replyOnce(404) // at first, it fails to fetch
  .onGet(API_URL)
  .reply(200, [{ language: "Javascript" }]); // then it successfully returns an array of objects, where each object refers to the user's projects; in this simple example, I'm just returning a single project

beforeEach(() => {
  mockSpy.mockClear(); // clear any previous calls to this spy mock
});

afterAll(() => {
  mockSpy.mockRestore(); // restore spy mock to original Swal.fire
});

it("fails to make an API request to retrieve a github user and displays a sweet alert error", async () => {
  render(<FavouriteLanguage />);

  // updates input with "example" username
  userEvent.type(screen.getByTestId("username"), username);

  // submits the form
  userEvent.click(screen.getByTestId("submit"));

  // using "waitFor" because submitting the form calls an async request
  // therefore you need to "waitFor" the request to resolve
  await waitFor(() => {
    // expect Swal.fire to be called with an error
    expect(Swal.fire).toHaveBeenCalledWith({
      title: "Error: Request failed with status code 404",
      icon: "error",
    });
  });
});

it("makes an API request to retrieve a github user's details and displays a sweet alert with user's favorite language.", async () => {
  render(<FavouriteLanguage />);

  userEvent.type(screen.getByTestId("username"), username);

  userEvent.click(screen.getByTestId("submit"));

  await waitFor(() => {
    // expect Swal.fire to be called with user's favorite language
    expect(Swal.fire).toHaveBeenCalledWith({
      text: "Javascript",
      title: "Example's Favourite Programming Language Is:",
      width: 625,
    });
  });
});

I tweaked your code a bit to make the above work, but you can retweak it to work with your code:

FavouriteLanguage.js

import { Component } from "react";
import axios from "axios";
// import sortKeysByValue from "sort-keys-by-value";
import Swal from "sweetalert2";
// import "./css/FavouriteLanguage.css";

class FavouriteLanguage extends Component {
  constructor(props) {
    super(props);
    this.state = {
      searchedUsername: "",
      programmingLanguagesUsed: "",
    };
  }

  searchGithubUsers = (event) => {
    this.setState({ searchedUsername: event.target.value });
  };

  getData = (e) => {
    e.preventDefault(); // make sure to call e.preventDefault when submitting a form; otherwise, it'll refresh the page

    const username = this.state.searchedUsername;
    axios
      .get(`https://api.github.com/users/${username}/repos?per_page=100`)
      .then((res) => {
        let allProgrammingLanguages = res.data
          .map((data) => data.language)
          .filter((lang) => lang !== null);

        this.setState({ programmingLanguagesUsed: allProgrammingLanguages });

        Swal.fire({
          title: `${
            username[0].toUpperCase() + username.slice(1).toLowerCase()
          }'s Favourite Programming Language Is:`,
          text: `${res.data[0].language}`,
          width: 625,
        });
      })
      .catch((error) => {
        Swal.fire({
          title: `${error.toString()}`,
          icon: "error",
        });
      });
  };

  render() {
    return (
      <div className="App">
        <header className="App-header">
          <div id="wrap">
            <h2>Find a user's favourite programming language</h2>
            <p>Please enter a valid Github username:</p>
            <form onSubmit={this.getData}>
              <input
                data-testid="username"
                type="text"
                placeholder="Github username..."
                onChange={this.searchGithubUsers}
              />
              <br />
              {/* 
                 Since you're already using a form, 
                 put the button inside of it and use type="submit".
                 Clicking the "submit" button calls "this.getData"!
               */}
              <button data-testid="submit" type="submit" className="btn">
                Submit
              </button>
            </form>
          </div>
        </header>
      </div>
    );
  }
}

export default FavouriteLanguage;

Result: enter image description here


推荐阅读