单元测试
文件夹结构
我们在测试文件夹和模型子文件夹下添加一个新的文件夹单元,在其中放置单元测试。文件夹结构与应用程序结构相匹配,可以很容易地找到相应的单元测试
webapp/test/unit/model/formatter.js(new)
/*global QUnit*/
sap.ui.define([
"sap/ui/demo/walkthrough/model/formatter",
"sap/ui/model/resource/ResourceModel"
], function (formatter, ResourceModel) {
"use strict";
QUnit.module("Formatting functions", {
beforeEach: function () {
this._oResourceModel = new ResourceModel({
bundleUrl: sap.ui.require.toUrl("sap/ui/demo/walkthrough") + "/i18n/i18n.properties"
});
},
afterEach: function () {
**this._oResourceModel.destroy();**
}
});
QUnit.test("Should return the translated texts", function (assert) {
// Arrange
// this.stub() does not support chaining and always returns the right data
// even if a wrong or empty parameter is passed.
var oModel = this.stub();
oModel.withArgs("i18n").returns(this._oResourceModel);
var oViewStub = {
getModel: oModel
};
var oControllerStub = {
getView: this.stub().returns(oViewStub)
};
// System under test
var fnIsolatedFormatter = formatter.statusText.bind(oControllerStub);
// Assert
assert.strictEqual(fnIsolatedFormatter("A"), "New", "The long text for status A is correct");
assert.strictEqual(fnIsolatedFormatter("B"), "In Progress", "The long text for status B is correct");
assert.strictEqual(fnIsolatedFormatter("C"), "Done", "The long text for status C is correct");
assert.strictEqual(fnIsolatedFormatter("Foo"), "Foo", "The long text for status Foo is correct");
});
});
我们在webapp/test/unit/model下创建了一个新的formatter.js文件,实现了自定义formatter的单元测试,formatter文件作为依赖项加载。如果检查翻译后的文本是否正确,我们就还需要一个ResourceModel的依赖。
格式化程序文件只包含一个用于格式化程序函数的QUnit模块。它用beforeEach函数中的本地化文本实例化我们的ResourceBundle,并在afterEach函数中再次销毁它。在每个测试执行之前和之后调用这些函数。
接下来是格式化程序函数的单元测试。在我们在步骤23中创建的statusText函数的实现中,我们通过以下队列调用来访问
ResourceBundle : var resourceBundle =this.getView().getModel(“i18n”).getResourceBundle();.
因为我们不想测试控制器、视图或模型的功能,所以我们首先在SinonJS和它的方法的帮助下,用空的外壳替换这些调用,移除依赖关系。这发生在单元测试的Arrange部分。SinonJS为所有对象注入了一个方法,所以我们可以简单地调用this.stub()来为我们需要模拟的任何行为创建一个新的桩。
测试桩是具有预编程行为的函数。除了可以用来改变桩行为的方法外,它们还支持完整的SinonJS测试监视API。如果这部分有点混乱,可以看看SinonJS测试检测的官方文档,或者现在忽略它,稍后就会清楚了。
然后,通过调用JavaScript的bind函数,将桩绑定到statusText格式化器。当使用变量fnIsolated Formatter调用函数时,this指针现在被绑定到控制器存根上,我们仍然可以按照自己的意愿传递参数。这发生在测试的“被测试系统”部分。
最后,我们执行断点。我们用数据模型中期望的值(A、B、C和其他所有值)调用独立的格式化程序函数,来检查格式化程序逻辑的每个分支。我们严格地将格式化器函数的结果与我们期望从资源包中获得的编码字符串进行比较,并在测试失败时给出有意义的错误消息。我们在这里对字符串进行硬编码,以识别资源包属性的问题。如果一个属性丢失了,如果我们检查实际值(两边都是空字符串),测试仍然会成功。
webapp/test/unit/unitTests.qunit.html (New)
<!DOCTYPE html>
<html>
<head>
<title>Unit tests for SAPUI5 Walkthrough</title>
<meta charset="utf-8">
<script
id="sap-ui-bootstrap"
src="https://openui5.hana.ondemand.com/resources/sap-ui-core.js"
data-sap-ui-resourceroots='{
"sap.ui.demo.walkthrough": "../../"
}'
data-sap-ui-async="true">
</script>
<link rel="stylesheet" type="text/css" href="https://openui5.hana.ondemand.com/resources/sap/ui/thirdparty/qunit-2.css">
<script src="https://openui5.hana.ondemand.com/resources/sap/ui/thirdparty/qunit-2.js"></script>
<script src="https://openui5.hana.ondemand.com/resources/sap/ui/qunit/qunit-junit.js"></script>
<script src="https://openui5.hana.ondemand.com/resources/sap/ui/qunit/qunit-coverage.js"></script>
<script src="https://openui5.hana.ondemand.com/resources/sap/ui/thirdparty/sinon.js"></script>
<script src="https://openui5.hana.ondemand.com/resources/sap/ui/thirdparty/sinon-qunit.js"></script>
<script src="unitTests.qunit.js"></script>
</head>
<body>
<div id="qunit"/>
<div id="qunit-fixture"/>
</body>
</html>
所谓的QUnit测试套件是一个HTML页面,它为应用程序触发所有QUnit测试。它的大部分是生成结果页面的布局,你可以在预览中看到,我们不会进一步解释这些部分,而是关注应用程序的部分。让我们从名称空间开始。因为我们现在是在webapp/test/unit文件夹中,我们实际上需要上升两层才能再次获得src文件夹。这个名称空间可以在测试中用于加载和触发应用程序功能。
首先,我们通过脚本标记加载一些基本的QUnit功能。其他QUnit测试也可以在这里添加。然后HTML页面加载另一个名为unitTests.qunit.js的脚本,我们接下来将创建它。这个脚本将执行我们的格式化程序。
webapp/test/unit/unitTests.qunit.js (New)
/* global QUnit */
QUnit.config.autostart = false;
sap.ui.getCore().attachInit(function () {
"use strict";
sap.ui.require([
"sap/ui/demo/walkthrough/test/unit/model/formatter"
], function () {
QUnit.start();
});
});
这个脚本加载并执行我们的格式化程序。如果我们现在在浏览器中打开webapp/test/unit/unitTests.qunit.html文件,我们应该看到我们的测试正在运行,并验证了格式化器逻辑。
注意:
所有的单元测试都放在应用程序的webapp/test/unit文件夹中。
测试套件中的文件以*.qunit.html结尾。
unitTests.qunit.html文件触发应用程序的所有单元测试。
应该为格式化程序、控制器逻辑和其他独立功能编写单元测试。
所有依赖项都被stub替换,只测试作用域内的功能。
与OPA的集成测试
如果我们想测试应用程序的交互模式或更多可视功能,我们还可以编写一个集成测试。
文件夹格式
我们在测试文件夹下面添加了一个新的文件夹集成,在那里我们放置了新的测试用例。帮助构造这类集成测试的页面对象被放在我们现在也创建的页面子文件夹中。
webapp/test/integration/NavigationJourney.js (New)
/*global QUnit, opaTest*/
sap.ui.define([
"sap/ui/demo/walkthrough/localService/mockserver",
"sap/ui/test/opaQunit",
"./pages/App"
], function (mockserver) {
"use strict";
QUnit.module("Navigation");
opaTest("Should open the Hello dialog", function (Given, When, Then) {
// initialize the mock server
mockserver.init();
// Arrangements
Given.iStartMyUIComponent({
componentConfig: {
name: "sap.ui.demo.walkthrough"
}
});
//Actions
When.onTheAppPage.iPressTheSayHelloWithDialogButton();
// Assertions
Then.onTheAppPage.iShouldSeeTheHelloDialog();
// Cleanup
Then.iTeardownMyApp();
});
});
让我们先从journey开始。一个 journey 由一系列属于同一上下文的集成测试组成,例如通过应用导航。类似于QUnit测试实现,OPA5使用QUnit,这就是为什么我们首先设置一个QUnit模块导航,它将显示在我们的结果页面上。函数OPA Test是用OPA定义集成测试的主要方面。它的参数定义了一个测试名称和一个回调函数,使用以下的OPA5助手对象执行,编写有意义的测试。
Given.
在给定的对象上,我们可以调用像 iStartMyUIComponent 这样的排列函数来加载我们的应用组件进行集成测试。
When.
包含我们可以执行的自定义操作,以使应用程序处于我们可以测试预期行为的状态。
Then.
包含检查应用程序中特定的自定义断点,以及再次删除组件的teardown函数。
在我们的过程中,我们创建了一个非常简单的测试来启动应用程序。在应用程序中,我们模拟点击一个按钮,然后期望对话框打开。最后,我们再次关闭应用程序。
正如您所看到的,测试用例读起来就像一个用户故事,我们实际上并不需要方法的实现来理解测试用例的含义。这种方法被称为“行为驱动开发”或简称BDD,在“敏捷软件”中很受欢迎。
webapp/test/integration/pages/App.js (New)
sap.ui.define([
"sap/ui/test/Opa5",
"sap/ui/test/actions/Press"
], function (Opa5, Press) {
"use strict";
var sViewName = "sap.ui.demo.walkthrough.view.HelloPanel";
Opa5.createPageObjects({
onTheAppPage: {
actions: {
iPressTheSayHelloWithDialogButton: function () {
return this.waitFor({
id: "helloDialogButton",
viewName: sViewName,
actions: new Press(),
errorMessage: "Did not find the 'Say Hello With Dialog' button on the HelloPanel view"
});
}
},
assertions: {
iShouldSeeTheHelloDialog: function () {
return this.waitFor({
controlType: "sap.m.Dialog",
success: function () {
// we set the view busy, so we need to query the parent of the app
Opa5.assert.ok(true, "The dialog is open");
},
errorMessage: "Did not find the dialog control"
});
}
}
}
});
});
页面对象的实现保存了我们刚才在旅程中调用的helper函数。我们要求sap.ui.test命名空间中的OPA5,并使用帮助函数createPageObjects定义一个页面对象。我们在 theappage 上传递了一个带有页面键的对象和两个部分:actions and assertions.。
在页面对象的动作部分,我们定义了一个函数来单击“Hello”对话框按钮。这是在OPA5中通过一个waitFor语句完成的,它基本上是一个循环,用于检查定义为参数的条件。如果条件满足,则执行成功回调,如果测试因为条件不满足而失败,errorMessage属性中的文本将显示在结果页面上。
我们定义了一个waitFor语句,用于检查类型为sap.m.Button的控件。只要在应用页面上找到一个按钮,就会执行成功处理程序,我们使用jQuery在找到的第一个按钮上触发点击事件。这应该会打开HelloDialog,类似于手动单击按钮。
在断点部分中,我们定义了另一个waitFor语句,用于检查应用程序的DOM中是否存在sap. m.d aldialog控件。当找到对话框时,测试就成功了,我们可以通过调用一个带有有意义消息的ok语句立即进行确认。
webapp/test/integration/opaTests.qunit.html (New)
<!DOCTYPE html>
<html>
<head>
<title>Integration tests for SAPUI5 Walkthrough</title>
<meta charset="utf-8">
<script
id="sap-ui-bootstrap"
src="https://openui5.hana.ondemand.com/resources/sap-ui-core.js"
data-sap-ui-theme="sap_belize"
data-sap-ui-resourceroots='{
"sap.ui.demo.walkthrough": "../../"
}'
data-sap-ui-animation="false"
data-sap-ui-compatVersion="edge"
data-sap-ui-async="true">
</script>
<link rel="stylesheet" type="text/css" href="https://openui5.hana.ondemand.com/resources/sap/ui/thirdparty/qunit-2.css">
<script src="https://openui5.hana.ondemand.com/resources/sap/ui/thirdparty/qunit-2.js"></script>
<script src="https://openui5.hana.ondemand.com/resources/sap/ui/qunit/qunit-junit.js"></script>
<script src="opaTests.qunit.js"></script>
</head>
<body>
<div id="qunit"></div>
<div id="qunit-fixture"></div>
</body>
</html>
此文件包含应用程序所有OPA测试的测试套件。我们使用与应用程序相同的名称空间。
然后,我们通过脚本标签从SAPUI5加载基本的QUnit功能,这样我们就可以执行测试旅程。我们上面定义的NavigationJourney将通过一个名为opaTests.qunit.js的脚本加载:
webapp/test/integration/opaTests.qunit.js (New)
/* global QUnit */
QUnit.config.autostart = false;
sap.ui.getCore().attachInit(function () {
"use strict";
sap.ui.require([
"sap/ui/demo/walkthrough/test/integration/NavigationJourney"
], function () {
QUnit.start();
});
});
这个脚本加载了NavigationJourney,并立即执行了其中的测试函数。当你在服务器上调用你的项目的webapp/test/integration/opaTests.qunit.html页面时,你应该看到QUnit布局和测试“应该看到Hello对话框”立即执行。它会在页面右侧加载应用组件。在那里,你可以看到测试在应用程序上执行的操作,如果一切正常,按钮点击被触发,然后显示一个对话框,测试用例是绿色的。
注意
OPA测试位于应用程序的webapp/test/integration文件夹中。
使用页面对象和行程来结构化OPA测试。
Debugging 工具
尽管我们在前面的步骤中添加了一个基本的测试覆盖率,但似乎我们不小心破坏了我们的应用,因为它不再在发票上显示价格了。我们需要在别人发现之前调试并修复这个问题。
webapp/view/InvoiceList.view.xml
<mvc:View
controllerName="sap.ui.demo.walkthrough.controller.InvoiceList"
xmlns="sap.m"
xmlns:mvc="sap.ui.core.mvc">
<List
id="invoiceList"
class="sapUiResponsiveMargin"
width="auto"
items="{
path : 'invoice>/Invoices',
sorter : {
path : 'ShipperName',
group : true
}
}">
<headerToolbar>
<Toolbar>
<Title text="{i18n>invoiceListTitle}"/>
<ToolbarSpacer/>
<SearchField width="50%" search=".onFilterInvoices"/>
</Toolbar>
</headerToolbar>
<items>
<ObjectListItem
title="{invoice>Quantity} x {invoice>ProductName}"
number="{
parts: [{path: 'invoice>ExTendedPrice'}, {path: 'view>/currency'}],
type: 'sap.ui.model.type.Currency',
formatOptions: {
showMeasure: false
}
}"
numberUnit="{view>/currency}"
**numberState="{= ${invoice>ExtendedPrice} > 50 ? 'Error' : 'Success' }">**
<attributes>
<ObjectAttribute text="{
path: 'invoice>Status',
formatter: '.formatter.statusText'
}"/>
</attributes>
</ObjectListItem>
</items>
</List>
</mvc:View>
我们在number属性的绑定中引入了一个拼写错误,以模拟一个经常出现的错误;而不是使用“invoice>ExtendedPrice”,我们使用
“invoice> ExTendedPrice”。
现在我们调用应用程序,注意到价格实际上丢失了。按CTRL+ALT+SHIFT+S,我们打开SAPUI5支持诊断工具,并检查应用程序。
如果你使用谷歌Chrome浏览器,你可以安装UI5 Inspector插件。有了这个插件,你可以轻松地调试你的基于SAPUI5或openui5的应用程序。要了解更多信息,请参见UI5 Inspector
除了关于应用程序的技术信息和类似于浏览器的开发人员工具控制台的跟踪之外,在这个对话框中还有一个非常方便的检查此类错误的工具。通过单击右边的展开符号来打开选项卡Control Tree。
左侧显示SAPUI5控件的层次树,右侧显示所选控件的属性。如果我们现在选择树的第一个ObjectListItem控件,并转到右边的Binding info选项卡,我们实际上可以看到number属性的绑定路径被标记为无效。我们现在可以更正视图中的错误,价格应该重新出现在发票清单中。
有时错误不那么容易发现,您实际上需要调试JavaScript代码与浏览器的工具。出于性能方面的原因,SAPUI5文件采用了一个精简版,这意味着所有可能的变量名都被缩短了,注释也被删除了。
这使得调试更加困难,因为代码的可读性大大降低。您可以通过添加URL参数 sap-ui-debug=true 或 按下 CTRL+ALT+SHIFT+P
并在弹出的对话框中选择“使用调试源”。重新加载页面后,可以在网络标签的浏览器的开发工具,现在许多文件与-dbg后缀被加载。这些是包含注释和应用程序的未压缩代码以及SAPUI5构件的源代码文件。
有关SAPUI5支持工具的更详细说明,请参阅故障排除教程。
如果您在某个开发任务上遇到困难并需要帮助,您也可以在与sapui5相关的论坛上发布问题,例如在SAP社区或Stack Overflow上。
注意
根据SAPUI5约定,未压缩的源文件以*-dbg.js结尾