Skip to Content
Technical Articles

UI5: Running sap.ui.core.util.MockServer server side using Puppeteer

UI5 provides a very useful Mock Server which can mimic any OData backend calls.

It can be used if no OData service is available or you simply don’t want to depend on the OData backend connectivity for your development and tests.

It is designed to simulate an OData provider by intercepting the HTTP communication made to the server, and providing a fake output. All this is transparent to the data binding and usage of OData model.

Though the Mock Server is only available on client side this perfectly suits above use cases.

But what if we want to go one step further towards production and want to test our app in different integration scenarios without having to deal with the actual service?

Could we use the Mock Server on server side as well?

Spoiler alert: Yes we can!

UI5 itself is a framework for building complete enterprise-ready and responsive web applications which run in any browser.

This implies that all its modules including the Mock Server are meant to run client side only.

So how could we get around this limitation?

Enter Puppeteer!

“Puppeteer is a Node library which provides a high-level API to control Chrome or Chromium over the DevTools Protocol. Puppeteer runs headless by default, but can be configured to run full (non-headless) Chrome or Chromium.”

Put simply Puppeteer is a Node.js module which can be used to run a web page in a headless Chrome instance providing some API to interact with the web page.

The idea would be to run our client side Mock Server via Puppeteer and use its API to forward any requests from an outer web server and then return the response.

Warning: As only Chrome will be our target browser and Node.js is also built on top of Chrome’s V8 engine we will make extensive use of ES6+ features like arrow functions, async…await and destructuring to make our lives easier.

Let’s start with the part we already know: the Mock Server itself.

We create a simple web page with the common data-sap-ui-bootstrap script tag:

<!DOCTYPE html>
<html>
<head>
	<meta charset="UTF-8">
	<title>localService</title>
  <script 
    id="sap-ui-bootstrap" 
    src="/resources/sap-ui-core.js"
		data-sap-ui-async="true"
		data-sap-ui-resourceroots='{
			"localService": "./localService"
		}'
		data-sap-ui-oninit="module:localService/mockserver"
	></script>
</head>
<body>
	<div>localService</div>
</body>
</html>

But as we do not want to show any UI and just want to start a single Mock Server instance we only reference our localService/mockserver module in the data-sap-ui-oninit hook:

sap.ui.define([
	'sap/ui/core/util/MockServer'
], (MockServer) => {
	'use strict';

	let oMockServer = new MockServer({
		rootUri: window.__ROOT_URI // will be injected by puppeteer
	});

	let sMetadataString = sap.ui.require.toUrl("localService/metadata.xml");
	var sMockdataBaseUrl = sap.ui.require.toUrl("localService/mockdata");
	oMockServer.simulate(sMetadataString, {
		sMockdataBaseUrl: sMockdataBaseUrl
	});

	oMockServer.start();
});

The only notable point here is the rootUri for our MockServer which will be globally defined and injected by Puppeteer. We will see how this works later, so stay tuned!

The next step is to serve our page including the necessary UI5 resources.

For this we gonna use the public API provided by the UI5 Build and Development Tooling modules.

All UI5 resources will be provided via npm as well. So there is no need to download any UI5 resources or SDK manually. But we only require @openui5/sap.ui.core.

With all the other modules we are going to use later our package.json looks like this:

{
  "name": "ui5-mockserver-puppeteer",
  "version": "0.1.0",
  "main": "index.js",
  "dependencies": {
    "@openui5/sap.ui.core": "^1.62.1",
    "@ui5/project": "^1.0.0",
    "@ui5/server": "^1.0.0",
    "body-parser": "^1.18.3",
    "express": "^4.16.4",
    "morgan": "^1.9.1",
    "puppeteer": "^1.12.2"
  },
  "config": {
    "port": 3000
  },
  "scripts": {
    "start" : "node index.js"
  }
}

 

With these dependencies we can easily create a helper module ui5-serve.js for serving our UI5 related project files by passing the current working directory and a port for starting the server:

'use strict'

const normalizer = require('@ui5/project').normalizer;
const server = require('@ui5/server').server;

module.exports = async options => {
	
	let tree = await normalizer.generateProjectTree({
		cwd: options.cwd
	});

	return server.serve(tree, {
		port: options.port,
		changePortIfInUse: true,
		acceptRemoteConnections: false
	});

};

And now comes the fun part! 🎉

With our MockServer set up and ready to be served we can access it using Puppeteer.

For this purpose we create a class UI5Page which encapsulates all functionality for starting the UI5 server and forwarding requests using Puppeteer. Any instance of this class will enable us to send AJAX requests to the Mock Server. To do so we first start the server and launch a browser instance using Puppeteer. Once both are ready we can open a new browser page and can inject the rootUri by defining it as a global variable in the browser page. Last we go to the browser page and wait until the page has been fully loaded to ensure our Mock Server is up and running. Finally we can send an AJAX request using Puppeteer’s page.evaluate function which allows us to execute any code in the page’s context. In our case we trigger a jQuery.ajax call (UI5 comes bundled with jQuery) which will be intercepted by the Mock Server. As all data needs to be serialized by Puppeteer anyway we ensure to use raw text for request and response data but will keep track of the content type.

'use strict';

// load modules
const puppeteer = require('puppeteer');

// load lib
const ui5Serve = require('./ui5-serve');

module.exports = class UI5Page {

	constructor(options) {
		this._cwd = options.cwd;
		this._port = options.port;
		this._rootUri = options.rootUri;
		this._page = null;
	}

	async _loadPage() {
		// start server & launch browser
		let [server, browser] = await Promise.all([
			ui5Serve({
				cwd: this._cwd,
				port: this._port
			}),
			puppeteer.launch()
		]);

		// open new page
		let page = await browser.newPage();

		// inject root uri
		page.evaluateOnNewDocument(rootUri => {
			Object.assign(window, {
				__ROOT_URI: rootUri
			});
		}, this._rootUri);

		// go to page
		await page.goto(`http://localhost:${server.port}/index.html`, {
			waitUntil: 'networkidle0' // ensure page has been fully loaded
		});

		return page;
	}

	async _evaluate() {
		// lazily load page
		if (!this._page) {
			this._page = await this._loadPage();
		}
		return this._page.evaluate.apply(this._page, [].slice.call(arguments));
	}

	ajax(reqMethod, reqUrl, reqBody) {
		return this._evaluate((method, url, body) => {
			// will be evaluated in the page's context
			return new Promise((resolve, reject) => {
				// trigger ajax request which will be intercepted by mockserver
				$.ajax({
						method: method,
						url: url,
						data: body,
						dataType: 'text'
					})
					.done((data, textStatus, jqXHR) => resolve({
						data: data,
						contentType: jqXHR.getResponseHeader('Content-Type')
					}))
					.fail((jqXHR, textStatus, errorThrown) => reject(new Error(errorThrown)));
			});
		}, reqMethod, reqUrl, reqBody);
	}
}

With our helper class in place we are ready to forward actual requests to it from an outer web server.

The easiest way is to create a dedicated express.Router ui5-router.js:

'use strict';

// load modules
const express = require('express');
const bodyParser = require('body-parser')
const puppeteer = require('puppeteer');

// load lib
const UI5Page = require('./UI5Page');

module.exports = options => {

	// create new page (incl. mock server)
	let ui5Page = new UI5Page(options);

	// a router object is an isolated instance of middleware and routes
	let router = express.Router();

	// parse any request body as text for forwarding
	router.use(bodyParser.text({
		type: '*/*'
	}));

	// handle all incoming requests
	router.all('/*', async (req, res, next) => {
		try {
			// forward request to page
			let result = await ui5Page.ajax(req.method, req.originalUrl, req.body);
			// send response with result returned by page
			res.set('Content-Type', result.contentType)
			res.end(result.data);
		} catch (error) {
			next(error);
		}
	});

	return router;
};

Our router uses body-parser for parsing any request body as raw text for later forwarding to the Mock Server.

Next we simply handle all incoming requests and forward them to our UI5Page instance and then send a response with the result data and content type returned.

Finally we just need to create and start a simple web server using express:

'use strict';

// load modules
const express = require('express');
const morgan = require('morgan');

// load lib
const ui5Router = require('./lib/ui5-router');

// config
const PORT = parseInt(process.env.npm_package_config_port);
const ROOT_URI = "/sap/opu/odata/sap/MEETUP_SRV/";

// create new app
let app = express();

// log incoming requests
app.use(morgan('dev'));

// forward requests to mockserver
app.use(ROOT_URI, ui5Router({
	cwd: __dirname,
	port: PORT + 1, // port for ui5 server (only used internally)
	rootUri: ROOT_URI
}));

// start server
let server = app.listen(PORT, () => console.log(`app is listening at http://localhost:${PORT}`));
server.on('error', error => console.error(`${error.message}`));

Let’s make sure that everything is working by browsing to:

http://localhost:3000/sap/opu/odata/sap/MEETUP_SRV/$metadata

(The first request will take some time to return a response because of our initial bootstrapping, subsequent request will be faster)

We should see our service’s metadata:

<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<edmx:Edmx Version="1.0"
    xmlns:edmx="http://schemas.microsoft.com/ado/2007/06/edmx">
    <edmx:DataServices
        xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata" m:DataServiceVersion="1.0">
        <Schema Namespace="NerdMeetup.Models"
            xmlns:d="http://schemas.microsoft.com/ado/2007/08/dataservices"
            xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata"
            xmlns="http://schemas.microsoft.com/ado/2006/04/edm">
            <EntityType Name="Meetup">
                <Key>
                    <PropertyRef Name="MeetupID" />
                </Key>
                <Property Name="MeetupID" Type="Edm.Int32" Nullable="false" />
                <Property Name="Title" Type="Edm.String" Nullable="true" />
                <Property Name="EventDate" Type="Edm.DateTime" Nullable="false" />
                <Property Name="Description" Type="Edm.String" Nullable="true" />
                <Property Name="HostedBy" Type="Edm.String" Nullable="true" />
                <Property Name="ContactPhone" Type="Edm.String" Nullable="true" />
                <Property Name="Address" Type="Edm.String" Nullable="true" />
                <Property Name="Country" Type="Edm.String" Nullable="true" />
                <Property Name="Latitude" Type="Edm.Double" Nullable="false" />
                <Property Name="Longitude" Type="Edm.Double" Nullable="false" />
                <Property Name="HostedById" Type="Edm.String" Nullable="true" />
                <Property Name="Location" Type="NerdMeetup.Models.LocationDetail" Nullable="false" />
            </EntityType>
            <ComplexType Name="LocationDetail" />
            <EntityContainer Name="NerdMeetups" m:IsDefaultEntityContainer="true">
                <EntitySet Name="Meetups" EntityType="NerdMeetup.Models.Meetup" />
                <FunctionImport Name="FindUpcomingMeetups" EntitySet="Meetups" ReturnType="Collection(NerdMeetup.Models.Meetup)" m:HttpMethod="GET" />
            </EntityContainer>
        </Schema>
    </edmx:DataServices>
</edmx:Edmx>

Let’s try another one:

http://localhost:3000/sap/opu/odata/sap/MEETUP_SRV/Meetups?$top=1

As we can see everything works as expected:

{"d":{"results":[{"MeetupID":1,"Title":"Toronto Tech Meet-Up","EventDate":"/Date(1593810000000)/","Description":"The best way to expand your knowledge and network of the Toronto technology community","__metadata":{"id":"/sap/opu/odata/sap/MEETUP_SRV/Meetups(1)","type":"NerdMeetup.Models.Meetup","uri":"/sap/opu/odata/sap/MEETUP_SRV/Meetups(1)"}}]}}

Any other requests supported by the Mock Server will also work. But be aware that any changes to mockdata are not persistent! For example any new entity created via POST will be gone after stopping the web server! So this approach should never be used in production!

But for testing purposes the service can easily deployed to e.g cloud foundry.

All project files can be found here:

https://github.com/pwasem/ui5-mockserver-puppeteer

👾 Happy Coding 👾

1 Comment
You must be Logged on to comment or reply to a post.