How do I imitate the Jest describe it pattern?

Viewed 212

How do I write the login and get functions in javascript? I have a feeling that it's possible with some mixture of inline functions, bind and this magic. Or is it impossible?

Promise.all([
  login("user1", () => {
    console.log(get("/healthy")); // prints "user1/healthy"
  }),
  login("user2", () => {
    console.log(get("/ready")); // prints "user2/ready"
  })
]);

I know it would be possible to write it like this. But I got curious about writing it without the obj.

login("user1", (obj) => {
  obj.get("/ready");
});

Isn't this similar to how Jest have coded the descript/it pattern?

describe("Login test", () => {
  test("Login", async () => {
    expect("ready").toEqual("ready");
  });
});
1 Answers

So you can technically make it work, but I don't recommend for reasons I'll explain later.

Here is a working example with the get function as a local variable. We assign to this get variable immediately before the callback is called.
It holds the login context in its closure scope. Because JavaScript is single threaded we know that the variable cannot be re-assigned a second time before the callback is run.

Here you can see it working with a random timeout to simulate a http call. The users and urls will be pair up correctly even though they execute async and in a random order. (Try running this snippet multiple times to checkout the output is always consistent.)

const sleep = () =>
  new Promise((resolve) => setTimeout(resolve, Math.random() * 1000));

let get;

async function login(username, callback) {
  console.log("logging in as", username);
  await sleep();
  get = async(url) => {
    await sleep();
    return `/${username}${url}`;
  };
  callback();
}

Promise.all([
  login("Alice", async() => {
    console.log(await get("/Active"));
  }),
  login("Bob", async() => {
    console.log(await get("/Build"));
  }),
  login("Colin", async() => {
    console.log(await get("/Compile"));
  }),
]);

The reason I don't recommend, is because this is very fragile code. We have to be very careful to make sure that the get function is called only at the start of the callback.

If for example we call sleep then get, all bets are off. We won't know which context get is using.

const sleep = () =>
  new Promise((resolve) => setTimeout(resolve, Math.random() * 1000));

let get;

async function login(username, callback) {
  console.log("logging in as", username);
  await sleep();
  get = async(url) => {
    await sleep();
    return `/${username}${url}`;
  };
  callback();
}

Promise.all([
  login("Alice", async() => {
    await sleep();   // <-- The only change from the code above. DANGER
    console.log(await get("/Active"));
  }),
  login("Bob", async() => {
    await sleep();
    console.log(await get("/Build"));
  }),
  login("Colin", async() => {
    await sleep();
    console.log(await get("/Compile"));
  }),
]);

So while this is all very interesting and fun to code, I believe the best option is just to be explicit about the obj context you are using (as you already described in your question) and save yourself a headache.

Related