问题
All I am doing in my code is upon componentDidMount
being run, I am making an axios get request to GitHub, and setting some data back onto state. However, when I run the test it still says the state is an empty array.
Here is my component below:
export default class HelloWorld extends Component {
constructor(props) {
super(props)
this.state = {
goodbye: false,
data: []
}
}
async componentDidMount() {
await this.func()
}
func = async () => {
let data = await axios.get('https://api.github.com/gists')
this.setState({data: data.data})
console.log(this.state)
}
goodbye = () => {
this.setState((state, currentProps) => ({...state, goodbye: !state.goodbye}))
}
render() {
return (
<Fragment>
<h1>
Hello World
</h1>
<button id="test-button" onClick={this.goodbye}>Say Goodbye</button>
{
!this.state.goodbye ? null :
<h1 className="goodbye">GOODBYE WORLD</h1>
}
</Fragment>
)
}
}
and here is my test:
it('there is data being returned', async () => {
const component = await mount(<HelloWorld />)
component.update()
expect(component.state('data')).toHaveLength(30)
})
I am very new to using Jest and am not sure what I am doing wrong. This app was built solely for testing out Jest with Enzyme. How can I test this component correctly?
回答1:
First of all you need to mock await axios.get('https://api.github.com/gists') somehow.
Them probably you'll need to do something like this after component.update() to "wait".
回答2:
Testing a component that runs an async function from componentDidMount
must await a re-render before running assertions. wrapper.update is used to sync Enzyme's component tree before running assertions but doesn't wait for promises or force a re-render.
One approach is to use setImmediate, which runs its callback at the end of the event loop, allowing promises to resolve non-invasively.
Here's a minimal example for your component:
import React from "react";
import Enzyme, {mount} from "enzyme";
import Adapter from "enzyme-adapter-react-16";
Enzyme.configure({adapter: new Adapter()});
import mockAxios from "axios";
import HelloWorld from "./HelloWorld";
jest.mock("axios");
describe("HelloWorld", () => {
beforeEach(() => jest.resetAllMocks());
it("should call `axios.get` and set the response to `state.data`", async () => {
const mockData = ["foo", "bar", "baz"];
mockAxios.get.mockImplementationOnce(() => Promise.resolve({data: mockData}));
const wrapper = mount(<HelloWorld />);
await new Promise(setImmediate);
wrapper.update();
expect(mockAxios.get).toHaveBeenCalledTimes(1);
expect(wrapper.instance().state.data).toEqual(mockData);
});
});
While this works, wrapper.instance().state.data
is too tied to the internal state of the application. I'd prefer to write a test that asserts on the behavior, not the implementation; variable name changes should not force tests to be re-written.
Here's an example component where the updated data is rendered in the DOM and tested in a more black-box manner:
Component (StackUsers.js
):
import axios from "axios";
import React from "react";
export default class StackUsers extends React.Component {
constructor(props) {
super(props);
this.state = {users: null};
}
componentDidMount() {
this.getUsers();
}
async getUsers() {
const ids = this.props.ids.join(";");
const url = `https://api.stackexchange.com/2.2/users/${ids}?site=stackoverflow`;
const res = await axios.get(url);
this.setState({users: res.data.items});
}
render() {
const {users} = this.state;
return (
<>
{users
? <ul data-test="test-stack-users-list">{users.map((e, i) =>
<li key={i}>{e.display_name}</li>
)}</ul>
: <div>loading...</div>
}
</>
);
}
}
Test (StackUsers.test.js
):
import React from "react";
import Enzyme, {mount} from "enzyme";
import Adapter from "enzyme-adapter-react-16";
Enzyme.configure({adapter: new Adapter()});
import mockAxios from "axios";
import StackUsers from "../src/StackUsers";
jest.mock("axios");
describe("StackUsers", () => {
beforeEach(() => jest.resetAllMocks());
it("should load users", async () => {
mockAxios.get.mockImplementationOnce(() => Promise.resolve({
data: {
items: [
{"display_name": "Jeff Atwood"},
{"display_name": "Joel Spolsky"},
]
},
status: 200
}));
const wrapper = mount(<StackUsers ids={[1, 4]} />);
let users = wrapper.find('[data-test="test-stack-users-list"]');
expect(users.exists()).toBe(false);
await new Promise(setImmediate);
wrapper.update();
expect(mockAxios.get).toHaveBeenCalledTimes(1);
users = wrapper.find('[data-test="test-stack-users-list"]');
expect(users.exists()).toBe(true);
expect(users.children()).toHaveLength(2);
expect(users.children().at(0).text()).toEqual("Jeff Atwood");
expect(users.children().at(1).text()).toEqual("Joel Spolsky");
});
});
Now, the only things the test knows is that axios.get
is used and the results should appear in an element with a specific data-test
attribute. This does not unduly interfere with CSS classes, HTML structure or component internals.
For reference, here are my dependencies:
}
"dependencies": {
"axios": "^0.18.0",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"react-redux": "^7.0.3",
"redux": "^4.0.1"
},
"devDependencies": {
"enzyme": "^3.9.0",
"enzyme-adapter-react-16": "^1.12.1",
"react-scripts": "^1.0.11",
"react-test-renderer": "^16.8.6"
}
}
来源:https://stackoverflow.com/questions/54465734/testing-asynchronous-componentdidmount-that-changes-state-with-jest-and-enzyme