-
-
Notifications
You must be signed in to change notification settings - Fork 3k
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
🚀 Feature: Context Variables and Functions #2743
Comments
Even better: scope could use defineProperty to define the property using an accessor descriptor on the context object, so that it doesn't have to be called using a function syntax! AWESOME! 😃 |
Re. that last idea, keep in mind that typo'd function calls are an error but typo'd properties are just Re. the proposal itself, I know we discussed this in Gitter, but I am having a hard time recalling (and it'd be nice to get it documented here as well): is there a short explanation of what advantage this has over just using local variables/constants of the describe("getUsers", function() {
beforeEach(function() { factory.createList(userCount(), 'user') });
const userCount = () => 1;
it("returns the right user fields", function() { ... });
context("when there are 10 users", function() {
const userCount = () => 10;
it("returns a first page of 5 users", function() { ... });
});
it("still has userCount 1 here", function() {
expect(userCount()).to.equal(1);
});
}); |
Okay, touche 😉 So my example was not very good, hehe. Obviously there's the syntactic sugar aspect, but that's not a huge difference, so here are the 2 cases where I don't think just using local variables would work: describe("case 1", function() {
var role = () => "user";
var user = () => {
// insert complicated HTTP mocking stuff you don't want to repeat in each context, or whatever
createUser(role());
}
it("is not an admin", function() {
expect(user()).not.to.be.an.admin();
}
context("as an admin", function() {
var role = () => "admin";
it("is an admin", function() {
expect(user()).to.be.an.admin(); // I'm pretty sure this fails...
});
});
}); function passesSharedSpecs() {
it("does something every context should do", function() {
expect(this.testableThing().property).to.eq("x"); // this shared spec doesn't have access to locals, so we have to reference `this.something`;
});
}
describe("case 2", function() {
var thingOptions = () => {version: 1};
before(function() {
this.testableThing = () => createThing(thingOptions()); // This will not use the right thingOptions in sub-contexts
setupMockResponseFor(testableThing());
});
passesSharedSpecs();
context("in another context", function() {
var thingOptions = () => {version: 2};
// ^^ We can't use a before block to set 'this.thingOptions' either, because it would execute after the previous before block that uses it.
passesSharedSpecs();
});
}); I believe my proposed feature solves both of these problems. |
Sorry, I could have been much more concise about what wouldn't work! I my original example (and yours above @ScottFreeCode) we're doing: expect(userCount()).to.equal(1); When the case that actually breaks is: // assuming we've assigned the result of the before block statement to this.users:
expect(this.users.count()).to.equal(1); Because the before block that's supposed to create N users doesn't pick up on the local variable |
In general, most of these seem like they could be made to work with local variables with sufficient restructuring -- for instance: describe("case 1", function() {
var makeUserFunction = role => {
// here do anything that you want set up once per role instead of once per call to user()
return () => createUser(role);
}
var user = makeUserFunction("user");
it("is not an admin", function() {
expect(user()).not.to.be.an.admin();
}
context("as an admin", function() {
var user = makeUserFunction("admin");
it("is an admin", function() {
expect(user()).to.be.an.admin();
});
});
}); function passesSharedSpecs(testableThing) {
it("does something every context should do", function() {
expect(testableThing().property).to.eq("x");
});
}
describe("case 2, no this", function() {
function makeTestableThing(thingOptions) {
var testableThing = () => createThing(thingOptions());
//setupMockResponseFor(testableThing()); I am ignoring this line for now because A) I am not sure what it's intended to do and B) if you call `testableThing()` for something used here and don't do the same in the nested context then whatever used it here will never get a version with a different `thingOptions` regardless of how `thingOptions` is changed
return testableThing;
});
passesSharedSpecs(makeTestableThing(() => {version: 1}));
describe("in another context", function() {
passesSharedSpecs(makeTestableThing(() => {version: 2}));
});
}); Refactoring would generally be our recommendation because it's almost always better for code to be more parameterizable and less dependent on state -- arguably the above higher-order functions are superior even to local variables, in that regard. But assuming for sake of hypotheticals that you had code that:
What if we tried to implement something like your scope variable proposal, but on the user side: // scope.js
module.exports = function Scope() {
var scopes = [{}]
return {
current: function() { return scopes[0] },
push: function(addition) { scopes.unshift(Object.assign(Object.create(scopes[0]), addition)) },
pop: function() { scopes.shift() }
}
}
// test.js
var scope = require('./scope.js')()
function passesSharedSpecs() {
it("does something every context should do", function() {
expect(scope.current().testableThing().property).to.eq("x");
});
}
describe("case 2", function() {
before(function() {
scope.current().testableThing = () => createThing(scope.current().thingOptions());
scope.push({
thingOptions: () => {version: 1};
});
setupMockResponseFor(scope.current().testableThing()); // still dubious, setupMockResponseFor will never be called with testableThing with version 2, but even a built-in Mocha `scope` would not fix that
});
after(scope.pop.bind(scope));
passesSharedSpecs();
context("in another context", function() {
before(function() {
scope.push({
thingOptions: () => {version: 2},
});
});
after(scope.pop.bind(scope));
passesSharedSpecs();
});
}); Thoughts? More generally, judicious use of a combination of outer variables with updates in |
Sure. I will agree that it can technically be done on the user side. The difference is that if I do it in a way that looks nice on the user side, I have to basically create my own DSL. Like, I would want to replace function passesSharedSpecs() {
it("does something every context should do", function() {
expect(scope.testableThing().property).to.eq("x");
});
}
describe("case 2", function() {
scope.thingOptions = {version: 1}; // doesn't really need to be a function - implementation of `scope` could reset this for every test even without using defineProperty
before(function() {
// when this block is called for "in another context" below, `scope.thingOptions` would be version 2
// So `testableThing` would be version 2, and `setupMockResponse` would call the mock responder (e.g. sinon) to setup a mock for version 2
scope.testableThing = () => createThing(scope.thingOptions);
setupMockResponseFor(scope.testableThing());
});
passesSharedSpecs();
context("in another context", function() {
scope.thingOptions = {version: 2};
passesSharedSpecs();
});
}); Incidentally, I think your most recent confusion with And the confusion with Anyway, if we could figure out a way to get close to that pretty using user-side code, I'd believe an argument that this isn't a good addition to Mocha, but I don't see how that would be possible without overriding the DSL... which I guess would be okay... maybe?? The thing that makes me think it would be easiest to add it to Mocha is that right now I'm just using |
I suppose scope may need to be a function(?), but my point is that I think most of these benefits could be accomplished by exposing and documenting a simple function from Mocha like: // in context blocks:
function scopeAccessor() {return this.ctx.scope}
// in test blocks
function scopeAccessor() {return this.currentTest.ctx.scope}
//everywhere:
function scope(variableName, value) {
if typeof(value) == "undefined"
return scopeAccessor()[variableName] = value;
else
return scopeAccessor()[variableName];
} (If that is a safe place to put the scope object... I'm not certain about that) |
The issue I was pointing out with In any case the main points I wanted to make here are:
Don't get me wrong, I can see how having a separate variable would be more versatile than
I should also add that we have had a couple similar proposals in the past and I vaguely recall that these sorts of counterobjections (it's doable using better alternatives, it's not the direction we want to promote) were provided by some of the more senior maintainers; given more time I might be able to dig them out of the issues history... To be clear: we might very well end up doing something like this, to create a more workable alternative to |
First of all thanks @ScottFreeCode! I really appreciate the consideration you've been putting into this! 😃 The function passesSharedSpecs() {
it("does something every context should do", function() {
expect(mockApi.findRequest).to.include(this.testableThing().properties);
});
}
describe("case 2", function() {
this.ctx.thingOptions = {version: 1};
beforeEach(function() {
this.testableThing = () => createThing(this.currentTest.ctx.thingOptions);
mockApi.setupResponseFor('path', this.testableThing());
browser.pushSomeButtons();
});
passesSharedSpecs();
it("passes other spec", function() {
expect(mockApi.response).to.have_status(401);
});
context("in another context", function() {
this.ctx.thingOptions = {version: 2};
passesSharedSpecs();
it("elicits a successful response", function() {
expect(mockApi.response).to.have_status(200);
});
it("displays a message with the updated data", function() {
expect(page.messages).to.include(some_data); // (requires mockApi to be setup correctly)
});
});
}); So I can imagine how to refactor this, by just creating a named function out of the To respond to your last two points quickly:
Mostly I agree that there are other ways to shrink redundancy even in these situations, when it becomes a real problem, but I am inclined to keep using |
Oh, and while it is only a convenience - not a necessity - I don't think that it is only a convenience for a special case. Sure its usefulness is only obvious in special cases, but in RSpec, using |
Well, that's a lot of good thoughts I'm going to have to chew over; thanks. 8^) I took another look at that one example given the understanding that that one block was supposed to be Anyhoo, I was thinking (based on that understanding) that the second example, if the outer hook was ...And found out that they are run in the right order, but the outer I have literally no clue what good this design was ever supposed to do, and I am now wondering how many issue reports we've gotten claiming that hooks run in the wrong order were misattributing to order what is fundamentally the uselessness of :sigh: So, where does that leave us... Well, for starters, I think I've answered my question, "How is the proposal different from the existing And I'm going to have to chew over the rest till I understand exactly what's being asked for, in order to start making recommendations about how best to get it -- whether through a Mocha update or through some userland pattern or construct. |
Indeed, not only is function passesSharedSpecs() {
it("does something every context should do", function() {
expect(this.testableThing().property).to.eq("x"); // this shared spec doesn't have access to locals, so we have to reference `this.something`;
});
}
describe("case 2", function() {
var thingOptions = () => ({version: 1}); // also corrected to return object instead of running block containing nothing but a labelled `1` statement (not sure why JS allows labels before statements other than blocks and loops, you'd think there'd be no opportunity for `1` to use the label...)
beforeEach(function() { // corrected to Each
this.testableThing = () => createThing(thingOptions());
setupMockResponseFor(testableThing());
});
passesSharedSpecs();
describe("in another context", function() {
var outerReset = {};
before(function() { // NB: runs just once before anything else in this suite, including outer Each hooks
outerReset.thingOptions = thingOptions;
thingOptions = () => ({version: 2}); // same arrow function syntax correction as above, same rant against JS allowing useless label
});
after(function() { // NB: runs just once after everything else in this suite
thingOptions = outerReset.thingOptions;
});
passesSharedSpecs();
});
}); ...And you only need describe("in another context", function() {
before(function() { // NB: runs just once before anything else in this suite, including outer Each hooks
thingOptions = () => ({version: 2}); // same arrow function syntax correction as above, same rant against JS allowing useless label
});
passesSharedSpecs();
}); Obviously, although this works, it is a little more imperative (at least where the variable needs to be reset afterward) and a little riskier, not to mention the theoretical bad-idea-ness of carrying around that state... |
Here's a quick question: If what you want, ultimately, is for functions to provide on-demand (well, not doing the work unless they're used) some resources that are initialized new for each test rather than carrying over from test to test, then do you need function passesSharedSpecs() {
it("does something every context should do", function() {
expect(this.testableThing().property).to.eq("x"); // this shared spec doesn't have access to locals, so we have to reference `this.something`;
});
}
describe("case 2", function() {
this.ctx.thingOptions = () => ({version: 1}); // also corrected to return object instead of running block containing nothing but a labelled `1` statement (not sure why JS allows labels before statements other than blocks and loops, you'd think there'd be no opportunity for `1` to use the label...)
this.ctx.testableThing = function() { // note use of regular function to be able to access the `this` parameter where the function is called from
var thing = createThing(this.thingOptions());
setupMockResponseFor(thing);
return thing;
};
passesSharedSpecs();
describe("in another context", function() {
this.ctx.thingOptions = () => ({version: 2}); // same arrow function syntax correction as above, same rant against JS allowing useless label
passesSharedSpecs();
});
}); And then, if you really want to avoid the I obviously don't use this sort of design pattern often enough in the first place that it took me three tries to get it right, first discovering that my initial idea was all wrong and then having to rediscover that my next thought that the thing is useless wasn't quite right either... |
Version with the more complete example: function passesSharedSpecs() {
it("does something every context should do", function() {
expect(mockApi.findRequest).to.include(this.testableThing().properties);
});
}
describe("case 2", function() {
this.ctx.thingOptions = {version: 1};
this.ctx.testableThing = function() { // needs to be regular function to get `this`
var thing = createThing(this.thingOptions);
mockApi.setupResponseFor('path', thing);
browser.pushSomeButtons();
return thing
};
passesSharedSpecs();
it("passes other spec", function() {
expect(mockApi.response).to.have_status(401);
});
describe("in another context", function() {
this.ctx.thingOptions = {version: 2};
passesSharedSpecs();
it("elicits a successful response", function() {
expect(mockApi.response).to.have_status(200);
});
it("displays a message with the updated data", function() {
expect(page.messages).to.include(some_data); // (requires mockApi to be setup correctly)
});
});
}); Also, congratulations, you've inspired a new construct in the homegrown smarter-setup-and-cleanup-management test runner I'm experimenting with. 8^) 👍 |
So, my personal opinions on how best to do this, on the assumption that "actually call the functions from the test that needs them so they can use
What do you think? (I am hoping I've finally got my head around the issue and the proposal enough that this idea's actually helpful, heh heh.) *I should mention that, due simply to using functions, there's room for optimization with this strategy too: since function results are sort of like lazy values (so you're already getting the "it isn't setup/computed unless the test uses it" optimization) and can be memoized (I can elaborate, but basically, there are ways that one of these resource-providing context-dependent functions could reuse existing objects on subsequent calls after they're first computed for a given context or set of inputs to creating said object if such reuse is correct and more efficient). |
Oh wow!
Well, okay, so I don't think that's actually true, but it IS true that Anyway, wow, yes, you totally have it figured out! :) I think I agree about your conclusions too. Automatic The one thing I don't like about that last example (just to further prove that aliasing Thanks for the further investigation! So... should I try to come up with an alias? I'd have to think a bit to imagine what would be both simple and extendable for the future. Do you like |
FWIW: The instinct I originally followed was to define my own simple alias like this in the global suite: beforeEach(function() {
this.context = this.currentTest.ctx;
}); ...so that I could use |
Good catch about The rest of this comment is written under the assumption that that's the case and would have to be revised if it's not; I haven't thoroughly tested it, but this outlines the stuff we'd want to try to test in light of what I'd like to try to accomplish. I'm going to run over a lot of stuff again, but hopefully this time all of it will be on point. So, speaking of ugly, it occurs to me that another value in an alias (or accessor method...) would be to unify these three ways of getting to the most-nested context:
So what if...
(Or, in other words, Tricky bit: If we didn't define Still... it seems like it should work. Memoization is a bit tricky, on reflection. Normally, you memoize on the arguments to the function. But in this case we want to memoize at least in part on the properties of function memoizedResource(thisDependencies, once) {
// use memoize from some other library
once = memoize(once)
return function wrapped() {
// arrow functions still get access to `wrapped`'s `this`
return once.apply(null, thisDependencies.map(entry => this.<alias>[entry]()))
}
}
...
this.<alias>.thingOption = () => ({version: 1})
this.<alias>.testableThing = memoizedResource(["thingOption"], function(thingOption) {
return createThing(thingOption)
})
beforeEach(function() {
mockAPI.whatever(this.<alias>.testableThing())
} Or, if built-in, like this: this.<accessor>("thingOption", [], () => ({version: 1}))
this.<accessor>("testableThing", ["thingOption"], function(thingOption) {
return createThing(thingOption)
})
beforeEach(function() {
mockAPI.whatever(this.<accessor>("testableThing")<()?>)
} Weird how that resembles AMD modules and dependency injection, but hey, it happened naturally, right? (I haven't thought in-depth about how to implement the built-in version -- the array lookup has to grab stuff out of the same scoped store instead of Anyway, should it be built-in? That would mean people get it who wouldn't have thought to implement it themselves. It would also mean that people have to understand they can't, say, use functions with side effects or that are expected to reset state like a Do we need a form that would not be memoized, or could you always access scoped-to-current-test data through this accessor in order to use it in performing non-memoized state resets in I like Final consideration: is it important to get the friendly name and/or built-in memoization if Mocha might change the This may be a moot point depending on research into other test runners, which I need to go do when I've got a little time. Any thoughts, concerns or suggestions at this point? If not, I think I'd like to write up a succinct rationale and design to summarize the various considerations and conclusions we've reached, so I can have the team review our final proposal in depth without having to read through my rambling chasing down red herrings in this thread. |
Great ideas 👍 So I'll call it Alias: Memoization: Actually, perhaps memoization isn't exactly the right word for what we want. Or at least, the concept feels slightly different. Because all we really need is that in each test, the method is only called once, and that result is returned for every subsequent call until the end of the test - even if the context has changed, or the value itself has been modified later in the test. And at the end of each test, all the results should be cleared for the next test. So my thought was that calling function scoped(name, func) {
if typeof(this.scopedResults[name]) == "undefined" {
this.scopedResults[name] = func(this.scope);
}
return this.scopedResults[name];
} Then, of course, there would have to be a global after hook that simply destroys I can't think of a use case for a non-memoized version... I think the potential use case you described is maybe alleviated (although I didn't fully understand it) by resetting all the stored results after each test? If |
This is basically what I was getting at. Traditional memoization reuses outputs based on the input parameters. We don't want parameters calling from the outside, we want other scoped values. (Basically -- although your further clarifications indicate this isn't quite what RSpec does -- I was thinking of not rerunning the function except when the other scoped values it depends upon are different, hence all the fiddling to get it to be memoized on whatever it needs to pull out of the scope.)
I probably should have said "break" rather than "circumvent" -- if your function returns the same result whenever the Of course, that could also have been resolved by including That being said...
You've anticipated what I've been pondering for the past couple days. 8^) While we can tell people they can't use it for resources that need to be reset with every test, I have been wondering whether that isn't something that's risky anyway in the same way that I hadn't thought of it before, but such pseudo-memoization as you suggest, something akin to memoization but based on the even more limited scope/lifetime not of suites but of tests, would be a good compromise: you don't accidentally recreate the thing many times if you use it in multiple places in a test, but you don't accidentally carry it over between tests either. And, come to think of it, that's the hardest form of something-like-memoization to implement without Mocha's help -- I am not sure how userland code would tell when the current test has changed (except Yeah, I really like this idea the more I think about it: don't actually memoize, but save the results and clear them on test completion.
Yup, I was thinking of memoization as in the same value is retrieved by multiple tests but considering the problem of some things needing to happen again for every test that depend on that value; "memoization" as in the same value is retrieved by multiple calls during the same test but it's wiped between tests has no such problem because it would happen again the first time for every test. So, sounds like we're good to go with that idea; not sure exactly how complex the implementation's going to be (we'll want to store the values somewhere that the test and hook callbacks can't access except through |
Perfect! Sounds great to me! 👍 |
Did this ever end up getting implemented? |
This is a huge thread with lots of detail, discussion, and nuance. Fascinating to read through. But, per #5027, we're not looking to significantly change Mocha for the time being. Given that this is doable in userland with wrapper functions or similar, and #1457 tracks adding a plugins API to make these kinds of experiments easier, closing as aged away. If anybody's reading this and dissapointed in the close, please do file a new issue with a concrete proposal & summary of how you got to that proposal. Cheers! |
Here's a proposal for how we can add contextual variables and functions to make
context
blocks more useful and reduce redundancy when the same variable or function is needed within several nested contexts.(Related to #2656, but solving the same problem by exposing functionality that avoids the pitfalls of global usage)
In Ruby:
In Ruby's RSpec, it is a very common (recommended) pattern to reduce code redundancy by extracting before-each blocks that would be repeated in sub-contexts and setting relevant variables within each of the relevant contexts to account for any minor differences:
The
let
function above is taking a block (function definition) and running it once per test, storing the result, and upon additional requests, returning the stored result (basically creating a singleton function out of the provided block) in order to:The defined functions are available to all child contexts, but can be overridden by any child context, (as shown above) so that they can minimize redundancy in cases where the same value is valid for multiple contexts.
In Mocha:
Almost the same thing can easily be accomplished currently by assigning an arrow function to a property of
thix.ctx
within the scope of acontext
block, and then accessing that function withthis.currentText.ctx
within abeforeEach
orit
block. This works because ctx is "cloned" (technically child contexts are created using the parent's context as a prototype), passing all properties to the child, and allowing them to be overridden without affecting the parent context:The differences are that:
this.ctx
) that it probably shouldn't be using without an exposed APIProposal
I propose making a variable called something like
scope
available in thecontext
block, which would allow the assigning of arrow functions, and in an actual test/before block, make a variable with the same name available, which has, as properties all the previously assigned functions as singleton functions, i.e.:Alternately, the scope available in a
context
block could be a function, so that you would set a scoped variable like this (which might make implementation easier?):The text was updated successfully, but these errors were encountered: