-
Notifications
You must be signed in to change notification settings - Fork 780
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Introduce data providers to QUnit #1568
Comments
@Krinkle any thoughts on this? |
The current approach in QUnit would be to use JavaScript's own native idioms, which result in an almost identical style. Perhaps even slightly more idiomatic by levering destructuring and template literals in a way that developers would recognise, understand, and know how to use without further explanation or dedicated documentation: [
[1, 1, 2],
[1, 2, 3],
[2, 1, 3],
].forEach(([a, b, expected]) => test(`add(${a}, ${b})`, assert => {
assert.equal(a + b, expected);
})); Personally, I tend to go for a slightly less dense style, but essentially the same: [
[1, 1, 2],
[1, 2, 3],
[2, 1, 3],
].forEach(([a, b, expected]) => {
q.test(`add ${a} and ${b}`, assert => {
assert.equal(a + b, expected);
});
}); This seems simple and close enough to not really warrant a new API, and possibly why it's not come up before. Most test frameworks in JavaScript and other languages do offer a form of data provider. But, in most languages and in some JS-based test runners that are more formal, it's not so easy to just handle this programmatically. E.g. in PHPUnit one couldn't use a foreach loop in the middle of a TestCase class to generate test cases. At the very least we should have a story for this on the website with examples! If we were to come up with our own, I think one of my wishes would be to make it look less "busy" than the above for the common case, and actually offer a meaningful abstraction layer that doesn't expose as many concepts as the DYI approach. Specifically:
A few ideas below. I toyed with having the base name go first, that might make for slightly better UX? I also tried both with and without variadic arguments. I like the simplicity of not needing to destructure, it looks cleaner. But, it also reserves this signature more or less forever for any other purpose, which makes it difficult for plugins to support this in a way that isn't confusing. For example, if we want to allow data sets to not all have the same number of parameters, then a plugin that "appends" a parameter to the signature would sometimes wrongly take the place of a dataset parameter. Maybe that's okay if we say we don't encourage/support plugins for this new method? q.test.each('add', {
foo: [ 1, 2, 3 ],
bar: [ 2, 3, 5 ]
}, (assert, a, b, expected) => {
assert.equal(a + b, expected);
});
// Test: add foo
// Test: add bar
q.test.each('add', [
[ 1, 2, 3 ],
[ 2, 3, 5 ]
], (assert, a, b, expected) => {
assert.equal(a + b, expected);
});
// Test: add #0
// Test: add #1 I think with these, I could confidently say that we've provided a simpler interface, with fewer concepts exposed that the user would need to understand or interact with. Thoughts? |
I've been approached with this as well in my domain, and I've also retorted with favoring the native/modern JS patterns. This is a related project to that end: The "sequential" and "combinatorial" flavors are certainly interesting, though again, not too far off of vanilla JS. Just something to keep in mind if we move forward with the |
Thanks @Krinkle, @smcclure15 :-).
I do not have a strong preference on this, but I would rather keep the test cases at the beginning (or at the end) of the arguments' list. This is because QUnit users are used to the I also like Let me know how you would like to proceed. As a next step I could add documentation to #1569. I'm assuming this would be its own page just like |
IMO We should document the straightforward javascript idiom and not add this to the qunit api Without any api changes, the same pattern can give you single test, multiple test, or module reuse without any new apis
|
@Krinkle, @smcclure15 any further thoughts on this? |
It's a close call for me, but I lean more towards builtin support via QUnit core, particularly with the "free" test-naming that @Krinkle proposed over the Copy/paste solutions breeds deviation, customizations, and mistakes, in addition to the sheer duplication. function parameterizedTest (parameters) {
const keys = Object.keys(parameters);
return (name, fcn) => {
keys.forEach(key => {
let values = parameters[key];
QUnit.test(`${name} (param=${key})`, assert => {
fcn(assert, ...values);
});
});
};
}
// USAGE:
const P_TEST = parameterizedTest({
foo: ['tagname1', 'myvalue1'],
bar: ['tagname2', 'myvalue1']
});
P_TEST('does ABC', (assert, tag, value) => {
...
});
// produces:
// does ABC (param=foo)
// does ABC (param=bar)
P_TEST('does XYZ', (assert, tag, value) => {
...
});
// produces
// does XYZ (param=foo)
// does XYZ (param=bar) That is favoring the I've also used Google Test and am familiar with its parameterization support. That uses a I like @Krinkle's idea to support objects and array, where the names are generated as I also wanted to think through a use case around some very simple API, like "isVowel" is one-input and a boolean output... QUnit.test.each('Vowel', ['a','e','i','o','u'], (assert, vowel) => {
assert.true(isVowel(vowel));
});
QUnit.test.each('Consonant', ['b','c','d','x','z'], (assert, consonant) => {
assert.false(isVowel(consonant));
});
QUnit.test.each('Invalid', ['word', '', 2, {}, null, undefined], (assert, bad) => {
assert.false(isVowel(bad));
}); The hard part is where we want to test an array value within those (eg, test that if (dataArray.length === dataArray.flat().length) { ? I think that "flat" workflow will be a popular, casual usage that is very readable in tests, and is important to support with as clear boundaries (when it's "flat" and when it requires a richer array/object specification) as possible. In general, I'd want to add as much input-validation as possible, to produce nice clean error messages when the API is accidentally misused; that's part of the real power and benefits of the builtin support. |
Thanks @smcclure15 I looked into Jest's support of "1D array of primitives" and they achieve that by checking that every item in the input array is an array itself (e.g.: if not Concerning naming, at the moment I'm just prefixing each test name with the test index. Perhaps we can look into more complex naming into a follow up PR? Both you and @Krinkle seem to prefer test inputs to be passed in between the test name and the callback function, so changed the API in #1569 to look like that. |
@smcclure15, @Krinkle: as #1569 seems to approach completion I wanted to discuss the follow up steps to support objects as input to QUnit.test.each('named object', {
foo: { from: 'a', to: 'b' },
bar: { from: 'x', to: 'y' }
}, (assert, { from, to }) => {
// …
});
// named object foo
// named object bar My interpretation here is that for each key in the input object (e.g.: Is there any other special case you would like me to support? Let me know if you prefer to discuss this in a separate issue. I am more than happy to create a new one if needed. |
@ventuno That looks right, yes. Just treating them the same as for array indexes. |
Yep! |
@Krinkle do you know when a new release (including this feature) is planned? |
@ventuno Today/tomorrow. Running through the checklist as I speak. |
Many test frameworks offer ways to avoid test duplication by allowing parameters to be passed into a test. For example, Jest offers
test.each
:TestNG (a Java test framework) offers the
DataProviders
which use decorators to attach data sources to tests.It makes sense for QUnit to offer the same feature.
Sample implementation
#1569
Open questions
printf
-like parameters (using Node'sutil.format
);describe.each
. I personally the benefit of this feature, but we should consider implementing our version ofmodule.each
.The text was updated successfully, but these errors were encountered: