freshtonic / given.js

Lazily evaluated variables for your specs

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

`given` doesn't allow raw values

timbertson opened this issue · comments

It's claimed in the README that this should work:

given('name', 'James Sadler');

But I get:

Error: definition of "name" is not a function
    at defineOneVariable (given/lib/given.js?body=1:128:15)
    at given (given/lib/given.js?body=1:156:16)

It works if I use the long form:

given('name', function() { return 'James Sadler'});

But that's not very convenient for non-coffeescript users.

Hi there. Apologies for the late reply, I must have missed the email notification. I'll look into this.

given intentionally stopped allowing raw values. It is as intended, README needs to be updated.

Maybe add

given.raw = function(data) {
  return function(){ return data; };
};

Then given('name', given.raw('James Sadler')});?

I recall there is a reason the raw values are not allowed and forced to delcare function.

To illustrate the challenge, then consider the following scenario:

given({
  first: "James",
  last: "Sadler",

  person: {
    name: this.first + " " + this.last  // <== what is "this" now?
  }
})

Then this in nested object is evaluated too early before giving given a chance to rebind this. By forcing to use function, Given can decide what this should be.

Maybe @freshtonic can confirm the reasoning.

@timbertson @raymaung see this commit for why it was removed as a feature: 6f70430

I'd accept a PR for a given.raw(...) implementation. It wouldn't advantage a CoffeeScript user because -> is short anyway, but for a JS user function() { return "myvalue"; } is a bit inconvenient.

I've just remembered why this isn't a good idea. It's because lazy evaluation won't work as one would expect.

given("foo", function() { return 123; });
given("bar", this.foo);  // this.foo evaluated immediately (not lazily)

given("foo", function() { return 456; });
given("baz", function() { return "foo value: " + this.bar; });

this.baz; // => "foo value: 123" instead of "foo value: 456"

This was a major pain point suffered by myself and my colleagues where I used to work, so I removed the non-lazy values. Even given.raw(...) won't solve it.

I'm closing this issue, given the reason above. I'll reopen it if someone comes up with a reasonable, elegant workaround to the problems introduced.

Maybe I'm expecting too little, but this doesn't really seem like much of a problem to me. A raw value is clearly non-lazy. I know what this is at the time I'm defining values, and it certainly isn't the given context object. So no, this example wouldn't work, and I don't think it could ever be made to work. But I would never have expected it to, either.

I guess the confusion here stems from the fact that there's always a this in javascript, but it requires users to mentally keep track of what this means in each scope, or else they run into issues like this. Perhaps one solution is to change how value functions are called.

What about if the context were given explicitly as an argument, rather than provided implicitly as this?

given('fullName', function(ctx) { return ctx.firstName + " " + ctx.lastName })

That way, nobody would ever think raw values could depend on other values since there's no way to access this ctx object outside of a lazy value. You need to do a little extra work to name the context, but only when you're depending on other properties. And the user no longer has to mentally track what this refers to various scopes.

I agree a better API would use a context instead of this for the definition functions. I was aiming for something more elegant, but clearly that has drawbacks such as remembering what this refers to in different parts of the code.

It may even be feasible to introduce optional context support without breaking backwards compatibility. By all means submit a PR. If it does break backwards compatibility then I'll release a version 2.0.0.

Sound reasonable?

Great - I've had a go, here's a PR: #5

I couldn't think of a good way to make the explicit context support backwards-compatible. Well I mean you could do definitionFn.call(env, env), but that probably only adds to the confusion since now there would be two ways to do the same thing.