项目前端使用 SAPUI5 开发,运行集成测试时,所有的 AJAX 请求都会被 Mock。今天遇到一个问题,一些 URL 改动后集成测试运行失败, 报错信息如下:
1 | Uncaught Error: INVALID_STATE_ERR - 4 |
尝试 Google Search,关键词 INVALID_STATE_ERR
,只搜到一些不是很相关的答案
- https://blogs.sap.com/2016/11/17/opa5-testing-with-json-backend/
- https://github.com/sinonjs/sinon/issues/493
转化思路 - Bug 是发生在 SAPUI5 的 MockServer 中,由于我对 MockServer 并不了解,因此我决定深入一下 MockServer
查阅 API 文档
打开 SAPUI5 API 文档,找到 MockServer, 这个对象继承自 ManagedObject,至于 ManagedObject 是干嘛的我目前还不太清楚,但是我决定先跳过这个细节。
MockServer 用来 Mock HTTP 请求。往下看 Constructor
用于创建一个 mocked server,可以 Mock XHR 和 OData/JSON Models。虽然我对 OData 还不是很懂,但是这应该与这个 BUG 关系不大,所以我跳过这个细节。Constructor 接受一些配置参数,
rootUri
requests
recordRequests
rootUri 是根路径,具体怎么用暂时比较模糊,稍后通过查阅源码来确定。requests 是一个数组,存放了被 mock 的请求(大致理解,稍后确认)。至于 recordRequests 是请求性能分析相关的,暂时忽略。
基于以上理解,一个 MockServer demo 如下:
1 | var aReqs = []; |
继续阅读 API 文档,Events 部分暂时跳过,来到 Methods 部分,项目中使用到了 sap.ui.core.util.MockServer.config 方法,该方法接受一个配置对象,其中几个属性如下:
- autoRespond
- autoRespondAfter
- fakeHTTPMethod
项目中的配置是
1 | { |
并有注释说明,autoRespondAfter 加随机数是用于模拟真实环境的网络时延,其中 oUriParameters
如下定义:
1 | var oUriParameters = new UriParameters(window.location.href); |
看个 Demo
看完文档,还是比较模糊,下一步我需要看个 Demo,本来我可以直接看项目中的 MockServer,只不过项目中的 MockServer 是比较复杂的,不利于理解。
根据官方 Demo 画了个流程图,
其中用到了 simulate 的方法。
深入源码
至此,我对 MockServer 有了一知半解,但是我还不知道 rootUri
, requests
和 autoRespond
的具体逻辑,所以我决定去看看 MockServer 的源码。克隆 openui5
1 | git clone https://github.com/SAP/openui5.git |
找到 src/sap.ui.core/src/sap/ui/core/util/MockServer.js
从 start 方法开始分析, 我们可以发现 start 方法会调 sinon.fakeServer.create
创建一个 FakeServer,然后再注册 mocked requests. 所以接下来我需要了解一下 sinon.fakeServer.
打开 sinon API 文档,找到FakeServer,使用很简单,直接看如下 demo,
1 | { |
其中两个 respondWith
和 respond
这两个方法需要我重点关注。回到 MockServer.js
的源码发现,_addRequestHanlder
方法最终调用 respondWith
(3446 行附近),
1 | this._oServer.respondWith(sMethod, oRegExp, fnResponse); |
从 API 文档得知,正则匹配到的 HTTP 方法将调用 fnResponse
,由该方法手动处理响应。
1 | server.respondWith("GET", /\/todo-items\/(\d+)/, function (xhr, id) { |
When the response is a
Function
, it will be passed the request object. You must manually call respond on it to complete the request.
文档还说,如果 respondeWith
中传递的response
是一个回调函数,那么执行回调时会传递 xhr 对象,并且需要回调手动调用 xhr.respond
方法发动响应。
现在,回头看项目中的 MockServer 的代码,
1 | var aReqs = []; |
其中还有一个循环将所有的 request 添加到 aReqs 中,
1 | aReqs.push(createRequest(data)); |
再看 createRequest(data)
方法,该方法返回一个对象
1 | function createRequest(data) { |
这个对象被 MockerServer.js
的 start 方法消费,看源码
该对象提供了 respondWith
中需要的三个参数,即 method, path 和 response。
至此, 我已经看懂了整个 MockServer 的过程了,再回头看错误信息,
看懂错误信息
错误由 XMLHttpRequest 对象引起,因此需要查阅 XHR 的 API 文档,搜索关键词 INVALID_STATE_ERR
共发现 8 处,
Throws an
INVALID_STATE_ERR
exception if the state is not OPENED or if thesend()
flag is true.
也就是说如果 XHR 的状态如果是 not opend 或者是已经 send 后的状态,那么就会抛出该异常。
结合错误信息给出的函数调用栈很容易定位出该错误发生在 setRespinseHeaders
之后,打开 sinon 源码去看 setResponseHeader 的实现,发现有个 verifyRequestOpend 方法。
再进入 verifyRequestOpend 方法一看,果然是从这里抛出的异常,异常原因就是 xhr 的状态已经变成 4 了,4 代表 DONE,详细状态参考这里。
1 | function verifyRequestOpened(xhr) { |
说明 298 行执行 xhr.resondJSON
之前已经执行过 xhr.send
方法了。
DEBUG
基于以上理解,打断点追踪一下就很容易发现代码逻辑有问题了(当然,这个代码不是本人写的,甩锅……)。
以上,记录一次 Debug 的过程。