[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">
|
<script type="text/javascript">
|
||||||
// List of modules, each module is preceded by its dependencies
|
// List of modules, each module is preceded by its dependencies
|
||||||
var oe_all_dependencies = ${dependencies};
|
var oe_all_dependencies = ${dependencies};
|
||||||
QUnit.config.testTimeout = 2000;
|
QUnit.config.testTimeout = 10000;
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body id="oe" class="openerp">
|
<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::
|
.. note::
|
||||||
|
|
||||||
asynchronous test cases also have a 2 seconds timeout: if the test
|
asynchronous test cases also have a 10 seconds timeout: if the
|
||||||
does not finish within 2 seconds, it will be considered
|
test does not finish within 10 seconds, it will be considered
|
||||||
failed. This pretty much always means the test will not resolve.
|
failed. This pretty much always means the test will not resolve.
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
@ -328,8 +328,8 @@ a test case (or its containing test suite) through
|
||||||
Mock RPC
|
Mock RPC
|
||||||
++++++++
|
++++++++
|
||||||
|
|
||||||
The preferred (and most fastest from a setup and execution time point
|
The preferred (and fastest from a setup and execution time point of
|
||||||
of view) way to do RPC during tests is to mock the RPC calls: while
|
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,
|
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
|
and only test the code between the "user" (the test itself) and the
|
||||||
RPC call, before the call is effectively done.
|
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
|
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
|
Testing API
|
||||||
-----------
|
-----------
|
||||||
|
|
|
@ -85,6 +85,7 @@ openerp.testing = {};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var db = window['oe_db_info'] || undefined;
|
||||||
testing.section = function (name, options, body) {
|
testing.section = function (name, options, body) {
|
||||||
if (_.isFunction(options)) {
|
if (_.isFunction(options)) {
|
||||||
body = options;
|
body = options;
|
||||||
|
@ -115,32 +116,69 @@ openerp.testing = {};
|
||||||
// returns -1 -> index becomes 0 -> replace with ``undefined`` so
|
// returns -1 -> index becomes 0 -> replace with ``undefined`` so
|
||||||
// Array#slice returns a full copy
|
// Array#slice returns a full copy
|
||||||
0, module_index + 1 || undefined);
|
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 () {
|
QUnit.test(name, function () {
|
||||||
// module testing environment
|
// module testing environment
|
||||||
var self = this;
|
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;
|
var instance;
|
||||||
if (!opts.dependencies) {
|
if (!opts.dependencies) {
|
||||||
|
@ -202,38 +240,80 @@ openerp.testing = {};
|
||||||
break;
|
break;
|
||||||
case 'rpc':
|
case 'rpc':
|
||||||
async = true;
|
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
|
// Always execute tests asynchronously
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
stop();
|
stop();
|
||||||
if (!_.isNumber(opts.asserts)) {
|
$.when(opts.setup(instance, $fixture, mock))
|
||||||
ok(false, "asynchronous test cases must specify the "
|
.pipe(function () {
|
||||||
+ "number of assertions they expect");
|
var result = callback(instance, $fixture, mock);
|
||||||
}
|
if (!(result && _.isFunction(result.then))) {
|
||||||
result.always(function () {
|
if (async) {
|
||||||
start();
|
ok(false, "asynchronous test cases must return a promise");
|
||||||
opts.teardown(instance, $fixture, mock);
|
}
|
||||||
}).fail(function (error) {
|
} else {
|
||||||
if (options.fail_on_rejection === false) {
|
if (!_.isNumber(opts.asserts)) {
|
||||||
return;
|
ok(false, "asynchronous test cases must specify the "
|
||||||
|
+ "number of assertions they expect");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
ok(false, typeof error === 'object' && error.message
|
return $.when(result).fail(function (error) {
|
||||||
? error.message
|
if (options.fail_on_rejection === false) {
|
||||||
: JSON.stringify([].slice.call(arguments)));
|
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);
|
})(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']);
|
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