我是测试驱动开发的新手,我遇到了有关测试/模拟 fetch api 的部分。 但我正在努力编写自己的测试。 我构建了一个简单的天气应用程序,只是为了使用笑话来测试/模拟获取。 但测试总是失败。我不断收到如下错误:
无效的挂钩调用。钩子只能在函数组件的主体内部调用。发生这种情况可能是由于以下原因之一: 不仅如此,我不知道我哪里出了问题,所以我来这里寻求有关如何模拟/改进我的测试以使其成功的提示。
这是我的 React 代码:(App.js)
const [search, setSearch] = useState('');
const [weather, setWeather] = useState({});
const handleChange = (e) => {
setSearch(e.target.value)
}
//function returns a promise
const WeatherData = async (e) => {
if (e.key === "Enter") {
await fetch(`${api.baseURL}weather?q=${search}&appid=${api.key}`)
.then(data => data.json())
.then(city => {
//console.log(city)
setSearch('')
setWeather(city)
})
}
}
const currentDate = (d) => {
let months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
let days = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
let day = days[d.getDay()];
let month = months[d.getMonth()];
let year = d.getFullYear();
let date = d.getDate();
return `${day} ${date} ${month} ${year}`
}
return (
<div className="App">
<h2>International Weather</h2>
<div className="wrapper">
<input type="text" id="search-field" placeholder='Search...' onChange={handleChange} onKeyPress={WeatherData} />
{(typeof weather.main != "undefined") ? (
<div className='weather-box'>
<h2>{weather.name}, {weather.sys.country}</h2>
<h2> {currentDate(new Date())} </h2>
<div id="weather">
<div className="details" id="degrees">{(weather.main.temp - 273.15).toFixed(2)}°C</div>
<div className="details" id="clouds">{weather.weather[0].main}</div>
</div>
</div>
) : (" ")}
</div>
</div>
);
}
还有我的 App.js 代码:
import { render, screen } from "@testing-library/react";
import App from "./App";
//creating a snapshot test to test if the rendered component is the same as the snapshot app
test("snapshot is correct", () => {
const tree = render(<App />);
expect(tree).toMatchSnapshot();
});
//test whether the function works
test("fetch works correctly", async () => {
App(
JSON.stringify({
results: [{ user: "mandla", age: 43 }],
})
).then((data) => {
expect(data).toBe();
});
});
如果有人能帮助我理解问题以及为什么我的解决方案不起作用,我将不胜感激。
您可以通过以下任意方法测试 fetch API。
// This is the function we'll be testing
async function withFetch() {
const res = await fetch('https://jsonplaceholder.typicode.com/posts')
const json = await res.json()
return json
}
// This is the section where we mock `fetch`
const unmockedFetch = global.fetch
beforeAll(() => {
global.fetch = () =>
Promise.resolve({
json: () => Promise.resolve([]),
})
})
afterAll(() => {
global.fetch = unmockedFetch
})
// This is actual testing suite
describe('withFetch', () => {
test('works', async () => {
const json = await withFetch()
expect(Array.isArray(json)).toEqual(true)
expect(json.length).toEqual(0)
})
})
const fetchMock = jest
.spyOn(global, 'fetch')
.mockImplementation(() =>
Promise.resolve({ json: () => Promise.resolve([]) })
)
describe('withFetch', () => {
test('works', async () => {
const json = await withFetch()
// highlight-start
expect(fetchMock).toHaveBeenCalledWith(
'https://jsonplaceholder.typicode.com/posts'
)
// highlight-end
expect(Array.isArray(json)).toEqual(true)
expect(json.length).toEqual(0)
})
})
请查看以下链接
开发了一个可以为每个测试用例初始化的获取解析器。
type Method = "get" | "options" | "post" | "put" | "patch" | "delete";
// https://httpstat.us
export enum Status {
OK = 200,
Created = 201,
Accepted = 202,
NonAuthoritativeInformation = 203,
NoContent = 204,
ResetContent = 205,
PartialContent = 206,
MultipleChoices = 300,
MovedPermanently = 301,
Found = 302,
SeeOther = 303,
NotModified = 304,
UseProxy = 305,
Unused = 306,
TemporaryRedirect = 307,
PermanentRedirect = 308,
BadRequest = 400,
Unauthorized = 401,
PaymentRequired = 402,
Forbidden = 403,
NotFound = 404,
MethodNotAllowed = 405,
NotAcceptable = 406,
ProxyAuthenticationRequired = 407,
RequestTimeout = 408,
Conflict = 409,
Gone = 410,
LengthRequired = 411,
PreconditionFailed = 412,
RequestEntityTooLarge = 413,
RequestURITooLong = 414,
UnsupportedMediaType = 415,
RequestedRangeNotSatisfiable = 416,
ExpectationFailed = 417,
Imateapot = 418,
MisdirectedRequest = 421,
UnprocessableEntity = 422,
Locked = 423,
TooEarly = 425,
UpgradeRequired = 426,
PreconditionRequired = 428,
TooManyRequests = 429,
RequestHeaderFieldsTooLarge = 431,
UnavailableForLegalReasons = 451,
InternalServerError = 500,
NotImplemented = 501,
BadGateway = 502,
ServiceUnavailable = 503,
GatewayTimeout = 504,
HTTPVersionNotSupported = 505,
VariantAlsoNegotiates = 506,
InsufficientStorage = 507,
NetworkAuthenticationRequired = 511,
Webserverisreturninganunknownerror = 520,
Connectiontimedout = 522,
Atimeoutoccurred = 524
}
/**
* Stub API request, response in test cases.
* - should be initialized and destroyed within the context of a specific case.
* - highly customizable
*
* <pre>
* describe("Fetch API", () => {
* let fetchResolver!: FetchResolver;
* beforeEach(() => {
* fetchResolver = new FetchResolver();
* });
*
* it("should load api", async () => {
* // stub
* fetchResolver.stub( "http://localhost:8080/endpoint", "post", { id: 100 }, { created: true }, 200);
* // fetch
* fetch("http://localhost:8080/endpoint",
* { method: "post", body: JSON.stringify({ id: 100 })}
* ).then((response) => {
* if (response.ok) {
* response.text().then((text) => {
* console.log(text); // { created: true }
* expect(text).toBeEqual({ created: true });
* });
* }
* });
* });
*
* afterEach(() => {
* fetchResolver.clear();
* });
* });
* </pre>
*
* Even though jest executes tests in parallel jest instance,
* We can't go wrong if stubs are cleaned after its use
*/
export class FetchResolver {
private mocks: Map<string, Response> = new Map();
constructor() {
this.init();
}
public stub(
uri: string,
method: Method,
payload: any,
response: any,
status: Status
) {
const finalRequest: { input: RequestInfo | URL; init?: RequestInit } = {
input: uri,
init: {
method: method,
body: JSON.stringify(payload)
}
};
console.log(
`mocking fetch :::\nrequest ${this.prettyPrint(
finalRequest
)} with \nresponse ${this.prettyPrint(response)} ans status ${status}`
);
this.mocks.set(
JSON.stringify(finalRequest),
new Response(JSON.stringify(response), { status: status })
);
}
private prettyPrint(json: any) {
return JSON.stringify(json, null, 2);
}
public clear() {
this.mocks.clear();
}
private init() {
jest
.spyOn(global, "fetch")
.mockImplementation((input: RequestInfo | URL, init?: RequestInit) => {
const request = {
input,
init
};
return new Promise((resolve, reject) => {
let response = this.mocks.get(JSON.stringify(request));
if (response) {
resolve(response);
} else {
// rejecting here will hurt component initialization
console.error(
`mock not implemented :::\nrequest ${this.prettyPrint(request)}`
);
// return empty response
resolve(new Response("{}"));
}
});
});
}
public static initialize() {
let resolver = new FetchResolver();
resolver.stub(
"http://localhost:8080/endpoint",
"post",
{ id: 100 },
{
created: true
},
200
);
fetch("http://localhost:8080/endpoint", {
method: "post",
body: JSON.stringify({ id: 100 })
}).then((response) => {
if (response.ok) {
response.text().then((text) => {
console.log(text); // { created: true }
});
}
});
}
}
这个问题过去已经被问过。我更喜欢使用像 https://github.com/jefflau/jest-fetch-mock
这样的库该库很灵活,有很多选择。