[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:
parent
108d617507
commit
bf4a26d3e8
|
@ -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 |
|
@ -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
|
||||
-----------
|
||||
|
|
|
@ -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,9 +116,10 @@ openerp.testing = {};
|
|||
// returns -1 -> index becomes 0 -> replace with ``undefined`` so
|
||||
// Array#slice returns a full copy
|
||||
0, module_index + 1 || undefined);
|
||||
QUnit.test(name, function () {
|
||||
// module testing environment
|
||||
var self = this;
|
||||
|
||||
// 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
|
||||
|
@ -125,22 +127,58 @@ openerp.testing = {};
|
|||
// 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");
|
||||
}
|
||||
var args = [].slice.call(arguments);
|
||||
return $.when(env._oe.setup.apply(null, args))
|
||||
.pipe(function () {
|
||||
return options.setup.apply(null, args);
|
||||
});
|
||||
},
|
||||
teardown: function () {
|
||||
if (options.teardown.apply(null, arguments)) {
|
||||
throw new Error("Asynchronous teardown not implemented");
|
||||
var args = [].slice.call(arguments);
|
||||
return $.when(options.teardown.apply(null, args))
|
||||
.pipe(function () {
|
||||
return env._oe.teardown.apply(null, args);
|
||||
});
|
||||
}
|
||||
if (self._oe.teardown(null, arguments)) {
|
||||
throw new Error("Asynchronous teardown not implemented");
|
||||
}, 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'
|
||||
}
|
||||
});
|
||||
}
|
||||
}, options, this._oe);
|
||||
|
||||
QUnit.test(name, function () {
|
||||
// module testing environment
|
||||
var self = this;
|
||||
|
||||
var instance;
|
||||
if (!opts.dependencies) {
|
||||
|
@ -202,31 +240,66 @@ 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);
|
||||
|
||||
// Always execute tests asynchronously
|
||||
stop();
|
||||
$.when(opts.setup(instance, $fixture, mock))
|
||||
.pipe(function () {
|
||||
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;
|
||||
}
|
||||
|
||||
stop();
|
||||
} else {
|
||||
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) {
|
||||
}
|
||||
return $.when(result).fail(function (error) {
|
||||
if (options.fail_on_rejection === false) {
|
||||
return;
|
||||
}
|
||||
|
@ -234,6 +307,13 @@ openerp.testing = {};
|
|||
? 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);
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue