stalniy/bdd-lazy-var

RSpec let! equivalent proposal

Opened this issue · 7 comments

The RSpec testing framework has lazy variable evaluation using let(:some_var) { ... } similar as this libary def("someVar", () => ...). But there's also an additional helper let!(:other_var) { ... } (note the exclamation mark) which will evaluate the variable, despite it being referenced in an example or not.

This comes in handy when a spec requires to setup some data which is not directly referenced. I'm currently doing to following for variables that need instantiation:

def("foo", () => "foo");
def("someBackgroundThing", () => "bar");

subject(() => someMethod($foo))

beforeEach(() => {
  $someBackgroundThing; // <---- referencing here so it's available
})

  it("works", () => {
    ...
  });

My proposal would be to introduce deff (or something similar) to replicate the "always instantiatie" behavior.

What do you think?

This is interesting. I see only one issue. this library allows to use lazy variables in beforeAll and afterAll hooks.

But for this specific helper, I can actually say that the behavior is different (anyway it's different), so this is fine.

However, I plan to rewrite library API to something like this. This will allow to greatly simplify integration with testing frameworks.

But not sure when this happens. Don't have enough capacity

I am +1 for this, but could live without it, too.

RSpec also has before/after and beforeAll/afterAll equivalents (before(:each)/before(:all)) as I'm sure you're aware, yet still allows for the non-lazy evaluation using let!(:varname) { "value here" }

A def! function (that could cause a definition be instantiated/evaluated whether it was used or not) may have very few instances (or perhaps none?) that can't be solved with referencing it in a before/beforeAll block, but nonetheless it would be helpful, and make this library "feel" even more like "RSpec for Javascript".

Scattered references I found regarding let! make it sound like RSpec evaluates all let!s in an "implicit before block". Not sure if that's a before(:each) or before(:all) block...but my gut says it would be a before(:all)?

If bdd-lazy-var is already able to tap into the lifecycle hooks and add a before or beforeAll hook that will evaluate all def! defined things, I think that's the move. I suppose it would need/want to happen before user-defined before or beforeAlls?

I am +1 for this, but could live without it, too.

RSpec also has before/after and beforeAll/afterAll equivalents (before(:each)/before(:all)) as I'm sure you're aware, yet still allows for the non-lazy evaluation using let!(:varname) { "value here" }

A def! function (that could cause a definition be instantiated/evaluated whether it was used or not) may have very few instances (or perhaps none?) that can't be solved with referencing it in a before/beforeAll block, but nonetheless it would be helpful, and make this library "feel" even more like "RSpec for Javascript".

Scattered references I found regarding let! make it sound like RSpec evaluates all let!s in an "implicit before block". Not sure if that's a before(:each) or before(:all) block...but my gut says it would be a before(:all)?

If bdd-lazy-var is already able to tap into the lifecycle hooks and add a before or beforeAll hook that will evaluate all def! defined things, I think that's the move. I suppose it would need/want to happen before user-defined before or beforeAlls?

That could be an approach! If would expect it to be before(:each), as different describe/context sections are able to override the variable as well.

Yeah, my RSpec is getting a little rusty, but before(:each) => beforeEach probably makes sense. For some reason I was thinking that the values persisted across test examples, but I don't think so...

I would like the equivalent of let! as well.

Syntax suggestion: a regular def with {eager: true} in its options or my preferred: {lazy: false}

My use case is

      beforeEach(async () => {
        await SomeSequelizeService.create($params); // This line returns an ORM object, I would like to save it's id.
      });

Not all methods in my context needs the id of the object or even the object at all, because they are testing other side effects of the call.

In ruby I would just

let!(:existing_object) { create :object}
let(:existing_object_id) { (existing_object.id }

I often have to work with async functions. And this forces me to write a lot of await statements.

describe("Board", function () {
  const ONE_YEAR_IN_SECS = 365 * 24 * 60 * 60;
  const ONE_GWEI = 1_000_000_000;

  def('contractFactory', async () => await ethers.getContractFactory("Board"));
  def('owner', async () => await ethers.getSigner());
  def('startTime', async () => await time.latest());
  
  def('unlockTime', async () => (await $startTime) + ONE_YEAR_IN_SECS);
  def('lockedAmount', () => ONE_GWEI);

  subject(async () => {
    return await (await $contractFactory).deploy($unlockTime, { value: $lockedAmount });
  });

  describe("Deployment", function () {
    const TWO_YEAR_IN_SECS = 365 * 24 * 60 * 60;
    
    def('unlockTime', async () => (await $startTime) + TWO_YEAR_IN_SECS);

    it(async () => {
      expect(await (await $subject).unlockTime()).to.equal(await $unlockTime)
    });
  });
});

It was possible to do without some async/await. I added them for clarity

It would be great if the variables were waiting for the promise to complete. But with lazy variables this is obviously not possible(#5). But it is possible with let! variables.

I suggest that when declaring a variable, add the optional ability to specify which variables must exist before this variable function is executed. And they are passed to function as parameters.

def!('thirdVarName', (firstVar, secondVar) => firstVar + secondVar, ['firstVarName', 'secondVarName']);

You can also specify lazy variables in this list. They are simply initialized as non-lazy along with this variable

Thus, even before starting the test, it will be possible to disassemble in the order of initialization of variables. And at the same time keep the var overriding in child describes

Example

describe("Board", function () {
  const ONE_YEAR_IN_SECS = 365 * 24 * 60 * 60;
  const ONE_GWEI = 1_000_000_000;

  def!('contractFactory', () => ethers.getContractFactory("Board"));
  def!('owner', () => ethers.getSigner());
  def!('startTime', () => time.latest());
  
  def('unlockTime', (startTime) => startTime + ONE_YEAR_IN_SECS, ['startTime']);
  def('lockedAmount', () => ONE_GWEI);

  subject!((contractFactory, unlockTime, lockedAmount) => {
    return contractFactory.deploy(unlockTime, { value: lockedAmount });
  }, ['contractFactory', 'unlockTime', 'lockedAmount']);

  describe("Deployment", function () {
    const TWO_YEAR_IN_SECS = 365 * 24 * 60 * 60;
    
    def('unlockTime', (startTime) => startTime + TWO_YEAR_IN_SECS, ['startTime']);

    it(async () => {
      expect(await $subject.unlockTime()).to.equal($unlockTime)
    });
  });
});