Behaviour-Assertion-Sheets
Behaviour-Assertion-Sheets copied to clipboard
NodeJS: Do not execute all rulesets
We've partially talked about this on ADN. To recap:
I want to be able to write a sheet file where every ruleset is meant for a specific URL. To avoid writing URLs twice I want to parse the URL from the ruleset's conditions. When that's done I want to loop through all rulesets. For each ruleset I would fetch the desired URL and then run the one ruleset over it.
My sheet file looks like this:
@page (url = "http://www.example.com/") {
status-code: 200;
#pageTitle h1 { text: /Welcome/; }
}
@page (url = "http://www.example.de/") {
status-code: 302;
}
@page (url = "http://www.example.de/de/") {
status-code: 200;
#pageTitle h1 { text: /Willkommen/; }
}
What I have so far: First I load a sheet and use rules.forEach
to get each one (simplified, my actual code includes a little error checking):
var bas = new BAS();
var suite = bas.loadSheet(sheet);
suite.yep(function() {
var rules = [];
bas.rules.forEach(function(rule) {
var url = myGetRuleUrl(rule);
rules.push({url: url, rule: rule});
});
myCallback(rules);
});
The function myGetRuleUrl
parses the rule.input and extracts the URL from the ruleset's conditions (it understands url, protocol, domain and path). In my use case I only use simple strings for comparison. Regular expressions would not work for what I have in mind.
The myCallback
function then loops through each result and does this (again, simplified for readability):
var testSuite = new BAS();
testSuite.loadSheet(sheet); // Same sheet from before
request(url, function(err, res, body) {
testSuite.run(url, res, body);
testSuite.on('end', function() {
// Result is handled here
});
});
So essentially I'm setting up a new Bas instance for every URL, and then I run the entire sheet over it, even though I already know that only one of the rulesets is going to match. Perhaps it is silly to worry over this since Bas is probably pretty quick about evaluating conditions on all the rulesets. But it's not elegant :)
I can think of two ways to simplify this.
First, add a method to add a rule object (as returned by BAS.rules.forEach
) to a test suite:
var testSuite = new BAS();
testSuite.addRule(rule);
Second option: Convert a rule object back into its string representation, so I can pass it back into BAS.loadSheet
:
var ruleString = rule.toString();
var testSuite = new BAS();
testSuite.loadSheet(ruleString);
I hope it's clear what I'm trying to accomplish.
But perhaps I'm approaching this from the wrong angle. I'm currently doing complicated things only to avoid writing the same URL twice.
Maybe I would be better off inventing a meta-syntax (or simple two-column database) for URL => ruleset. Then each ruleset could simply be of the @all
type and I would have no problem running each test separately.
I think it's reasonable to want to run top-level rulesets on a discretionary basis. I could supply a run
method on the Rulesets (each one of bas.rules
) that would take the same arguments as Bas.run
. Would that suit your needs?
In regards to a meta-syntax: have you looked into Bas' annotation system? You could do something like this in your sheet:
/*@url:http://example.com/abc?def=123*/
@all {
status-code: 200;
}
/*@url:http://example2.net/ghi?klm=456*/
@all {
status-code: 302;
}
And then in your core code you could extract the URLs like so:
testSuite.rules.forEach(function(rule) {
var urls =
rule.annotations
.filter(function(annotation) {
return annotation.match(/^\s*url\:/i);
})
.map(function(annotation) {
return (
annotation
.replace(/(^\s+|\s+$)/g)
.substr(4)
);
});
urls.forEach(function(url) {
request(url,function(err, res, body) {
if (err) { /* handle err */ }
rule.run(url, res, body);
});
});
});
(Please note, I haven't tested that, and it depends on the currently fictitious rule.run
method, but it serves to demonstrate how I would approach the problem.)
As a side note, please be aware that Bas.loadSheet
is an async operation (it uses an async fs call.) This means that if you want to avoid race conditions, you'll need to supply a node-style callback or handle the returned promise like so:
testSuite.loadSheet("example.bas")
.yep(function() { /* handle success */ })
.nope(function() { /* handle failure */ });
rule.run
would fit perfectly. Your example code could replace most of the code I wrote around Bas :)
I hadn't looked at annotations. As mentioned I parse the URL from @page (url = "http://example.com")
, which works similarly well for me. But I think your example code is neater. Plus, your code could be expanded so that I could have multiple URLs use the same ruleset.
As for loadSheet
, I simply forgot to use yep/nope in one place. Thanks for the reminder.