[ADD] actual-RPC tests, with UI. Bump QUnit timeout to 10s so RPC can run at all

bzr revid: xmo@openerp.com-20121029110154-5927gaix8k0ijl0c
This commit is contained in:
Xavier Morel 2012-10-29 12:01:54 +01:00
parent 108d617507
commit bf4a26d3e8
8 changed files with 242 additions and 57 deletions

View File

@ -53,7 +53,7 @@ TESTING = Template(u"""<!DOCTYPE html>
<script type="text/javascript">
// List of modules, each module is preceded by its dependencies
var oe_all_dependencies = ${dependencies};
QUnit.config.testTimeout = 2000;
QUnit.config.testTimeout = 10000;
</script>
</head>
<body id="oe" class="openerp">

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -297,8 +297,8 @@ test architecture was not warned about asynchronous operations.
.. note::
asynchronous test cases also have a 2 seconds timeout: if the test
does not finish within 2 seconds, it will be considered
asynchronous test cases also have a 10 seconds timeout: if the
test does not finish within 10 seconds, it will be considered
failed. This pretty much always means the test will not resolve.
.. note::
@ -328,8 +328,8 @@ a test case (or its containing test suite) through
Mock RPC
++++++++
The preferred (and most fastest from a setup and execution time point
of view) way to do RPC during tests is to mock the RPC calls: while
The preferred (and fastest from a setup and execution time point of
view) way to do RPC during tests is to mock the RPC calls: while
setting up the test case, provide what the RPC responses "should" be,
and only test the code between the "user" (the test itself) and the
RPC call, before the call is effectively done.
@ -412,7 +412,83 @@ To do this, set the :js:attr:`rpc option <~TestOptions.rpc>` to
Actual RPC
++++++++++
.. TODO:: rpc to database (implement & document)
A more realistic (but significantly slower and more expensive) way to
perform RPC calls is to perform actual calls to an actually running
OpenERP server. To do this, set the :js:attr:`rpc option
<~TestOptions.rpc>` to ``rpc``, it will not provide any new parameter
but will enable actual RPC, and the automatic creation and destruction
of databases (from a specified source) around tests.
First, create a basic model we can test stuff with:
.. code-block:: javascript
from openerp.osv import orm, fields
class TestObject(orm.Model):
_name = 'web_tests_demo.model'
_columns = {
'name': fields.char("Name", required=True),
'thing': fields.char("Thing"),
'other': fields.char("Other", required=True)
}
_defaults = {
'other': "bob"
}
then the actual test::
test('actual RPC', {rpc: 'rpc', asserts: 4}, function (instance) {
var Model = new instance.web.Model('web_tests_demo.model');
return Model.call('create', [{name: "Bob"}])
.pipe(function (id) {
return Model.call('read', [[id]]);
}).pipe(function (records) {
strictEqual(records.length, 1);
var record = records[0];
strictEqual(record.name, "Bob");
strictEqual(record.thing, false);
// default value
strictEqual(record.other, 'bob');
});
});
This test looks like a "mock" RPC test but for the lack of mock
response (and the different ``rpc`` type), however it has further
ranging consequences in that it will copy an existing database to a
new one, run the test in full on that temporary database and destroy
the database, to simulate an isolated and transactional context and
avoid affecting other tests. One of the consequences is that it takes
a *long* time to run (5~10s, most of that time being spent waiting for
a database duplication).
Furthermore, as the test needs to clone a database, it also has to ask
which database to clone, the database/super-admin password and the
password of the ``admin`` user (in order to authenticate as said
user). As a result, the first time the test runner encounters an
``rpc: "rpc"`` test configuration it will produce the following
prompt:
.. image:: ./images/db-query.png
:align: center
and stop the testing process until the necessary information has been
provided.
The prompt will only appear once per test run, all tests will use the
same "source" database.
.. note::
The handling of that information is currently rather brittle and
unchecked, incorrect values will likely crash the runner.
.. note::
The runner does not currently store this information (for any
longer than a test run that is), the prompt will have to be filled
every time.
Testing API
-----------

View File

@ -85,6 +85,7 @@ openerp.testing = {};
};
};
var db = window['oe_db_info'] || undefined;
testing.section = function (name, options, body) {
if (_.isFunction(options)) {
body = options;
@ -115,32 +116,69 @@ openerp.testing = {};
// returns -1 -> index becomes 0 -> replace with ``undefined`` so
// Array#slice returns a full copy
0, module_index + 1 || undefined);
// Serialize options for this precise test case
// WARNING: typo is from jquery, do not fix!
var env = QUnit.config.currentModuleTestEnviroment;
var opts = _.defaults({
// section setup
// case setup
// test
// case teardown
// section teardown
setup: function () {
var args = [].slice.call(arguments);
return $.when(env._oe.setup.apply(null, args))
.pipe(function () {
return options.setup.apply(null, args);
});
},
teardown: function () {
var args = [].slice.call(arguments);
return $.when(options.teardown.apply(null, args))
.pipe(function () {
return env._oe.teardown.apply(null, args);
});
}
}, options, env._oe);
// FIXME: if this test is ignored, will still query
if (opts.rpc === 'rpc' && !db) {
QUnit.config.autostart = false;
db = {
source: null,
supadmin: null,
password: null
};
var $msg = $('<form style="margin: 0 1em 1em;">')
.append('<h3>A test needs to clone a database</h3>')
.append('<h4>Please provide the source clone information</h4>')
.append(' Source DB: ').append('<input name="source">').append('<br>')
.append(' DB Password: ').append('<input name="supadmin">').append('<br>')
.append('Admin Password: ').append('<input name="password">').append('<br>')
.append('<input type="submit" value="OK"/>')
.submit(function (e) {
e.preventDefault();
e.stopPropagation();
db.source = $msg.find('input[name=source]').val();
db.supadmin = $msg.find('input[name=supadmin]').val();
db.password = $msg.find('input[name=password]').val();
QUnit.start();
$.unblockUI();
});
$.blockUI({
message: $msg,
css: {
fontFamily: 'monospace',
textAlign: 'left',
whiteSpace: 'pre-wrap',
cursor: 'default'
}
});
}
QUnit.test(name, function () {
// module testing environment
var self = this;
var opts = _.defaults({
// section setup
// case setup
// test
// case teardown
// section teardown
setup: function () {
if (self._oe.setup.apply(null, arguments)) {
throw new Error("Asynchronous setup not implemented");
}
if (options.setup.apply(null, arguments)) {
throw new Error("Asynchronous setup not implemented");
}
},
teardown: function () {
if (options.teardown.apply(null, arguments)) {
throw new Error("Asynchronous teardown not implemented");
}
if (self._oe.teardown(null, arguments)) {
throw new Error("Asynchronous teardown not implemented");
}
}
}, options, this._oe);
var instance;
if (!opts.dependencies) {
@ -202,38 +240,80 @@ openerp.testing = {};
break;
case 'rpc':
async = true;
(function (setup, teardown) {
// Bunch of random base36 characters
var dbname = 'test_' + Math.random().toString(36).slice(2);
opts.setup = function (instance, $s) {
// FIXME hack: don't want the session to go through shitty loading process of everything
instance.session.session_init = testing.noop;
instance.session.load_modules = testing.noop;
instance.session.session_bind();
return instance.session.rpc('/web/database/duplicate', {
fields: [
{name: 'super_admin_pwd', value: db.supadmin},
{name: 'db_original_name', value: db.source},
{name: 'db_name', value: dbname}
]
}).pipe(function (result) {
if (result.error) {
return $.Deferred().reject(result.error).promise();
}
return instance.session.session_authenticate(
dbname, 'admin', db.password, true);
}).pipe(function () {
return setup(instance, $s);
});
};
opts.teardown = function (instance, $s) {
return $.when(teardown(instance, $s)).pipe(function () {
return instance.session.session_logout()
}).pipe(function () {
return instance.session.rpc('/web/database/drop', {
fields: [
{name: 'drop_pwd', value: db.supadmin},
{name: 'drop_db', value: db.dbname}
]
});
}).pipe(function (result) {
if (result.error) {
return $.Deferred().reject(result.error).promise();
}
return result;
});
};
})(opts.setup, opts.teardown);
}
// TODO: async setup/teardown
opts.setup(instance, $fixture, mock);
var result = callback(instance, $fixture, mock);
// TODO: cleanup which works on errors
if (!(result && _.isFunction(result.then))) {
if (async) {
ok(false, "asynchronous test cases must return a promise");
}
opts.teardown(instance, $fixture, mock);
return;
}
// Always execute tests asynchronously
stop();
if (!_.isNumber(opts.asserts)) {
ok(false, "asynchronous test cases must specify the "
+ "number of assertions they expect");
}
result.always(function () {
start();
opts.teardown(instance, $fixture, mock);
}).fail(function (error) {
if (options.fail_on_rejection === false) {
return;
$.when(opts.setup(instance, $fixture, mock))
.pipe(function () {
var result = callback(instance, $fixture, mock);
if (!(result && _.isFunction(result.then))) {
if (async) {
ok(false, "asynchronous test cases must return a promise");
}
} else {
if (!_.isNumber(opts.asserts)) {
ok(false, "asynchronous test cases must specify the "
+ "number of assertions they expect");
}
}
ok(false, typeof error === 'object' && error.message
? error.message
: JSON.stringify([].slice.call(arguments)));
})
return $.when(result).fail(function (error) {
if (options.fail_on_rejection === false) {
return;
}
ok(false, typeof error === 'object' && error.message
? error.message
: JSON.stringify([].slice.call(arguments)));
})
}).pipe(function () {
return opts.teardown(instance, $fixture, mock);
}, function () {
return opts.teardown(instance, $fixture, mock);
}).always(function () {
start();
});
});
};
})(openerp.testing);

View File

@ -0,0 +1,14 @@
from openerp.osv import orm, fields
class TestObject(orm.Model):
_name = 'web_tests_demo.model'
_columns = {
'name': fields.char("Name", required=True),
'thing': fields.char("Thing"),
'other': fields.char("Other", required=True)
}
_defaults = {
'other': "bob"
}

View File

@ -84,4 +84,19 @@ openerp.testing.section('basic section', function (test) {
deepEqual(dbm.db_list, ['foo', 'bar', 'baz']);
});
});
test('actual RPC', {rpc: 'rpc', asserts: 4}, function (instance) {
var Model = new instance.web.Model('web_tests_demo.model');
return Model.call('create', [{name: "Bob"}])
.pipe(function (id) {
return Model.call('read', [[id]]);
}).pipe(function (records) {
strictEqual(records.length, 1);
var record = records[0];
strictEqual(record.name, "Bob");
strictEqual(record.thing, false);
// default value
strictEqual(record.other, 'bob');
});
});
});