Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
May 24, 2022 07:16 pm GMT

Serious Jest: Making Sense of Hoisting

The tickets almost done, I just need to write a few tests
Later . . .

Image description

Hoisting Isnt Always Hoisting

Even if youre using Jest, you may not have thought too much about how Jest actually replaces your module with a mock. At least until youre mocks dont work.

Jest creates mocks through a process they refer to as hoisting. If youve used Javascript long enough you might assume you know what that means. But something I haven't seen elsewhere hoisting in Jest and hoisting in Javascript isnt the same thing. In this article, Ill explain both types of hoisting and walk through what happens each time your run a test.

Upcoming

  1. Four ways we might mock a component
  2. Jest Hoisting is Execution Order Manipulation
  3. Javascript Hoisting is Reference Assignment Manipulation
  4. Six Stages of Running a test
  5. Four ways to mock a component revisited

Four Ways We Might Mock A Component

For the sake of example, well make a component called MoviePoster. Take a look at it below:

// MoviePoster.jsximport { useDolphLundgren } from "ThunderGunExpress";export function MoviePoster() {    const ThunderGun = useDolphLundgren();    return (        <PosterContent>          <ThunderGun />        </PosterContent>    )}// MoviePoster.test.jsximport { useDolphLundgren } from "ThunderGunExpress";describe("HomePage", () => {    it("Should not hesistate", () => {      // ...     });});

MoviePoster uses ThunderGunExpress to make an API call through a hook called useDolphLundgren. useDolphLundgren, returns a component we can pass as a child to a PosterContent. But our test suite cant reproduce the output from the useDolphLundgren hook so we'll need to mock it. Lets look at some ways in which we might mock that hook.

Jest Best Guess

Lets look at four examples of how we might mock out useDolphLundgren using Jests factory mocks. Take a look at each example, guess whether it will work, and then guess why

Ex 1: Inline mock

jest.mock("ThunderGunExpress", () => ({    useDolphLundgren: () => <></>,}));

Ex 2: Passing a function declaration

function MockHook(props) {    return <></>};jest.mock("ThunderGunExpress", () => ({    useDolphLundgren: MockHook,}));

Ex 3: Passing a function expression

const MockHook = (props) => {    return <></>};jest.mock("ThunderGunExpress", () => ({    useDolphLundgren: MockHook,}));

Ex 4: Calling a function expression within another function expression

const MockHook = (props) => {    return <></>};jest.mock("ThunderGunExpress", () => ({    useDolphLundgren: () => MockHook(),}));

Jest Best Guess Addressed

Example 1 fails with an error telling you that you cant rely on that fragment youve imported to exist.

Example 2 works because the function definition is hoisted and exists when the useDolphLundgren mock gets created.

Example 3 fails because the MockHook variable is undefined when the mock function is created.

Example 4 works because the function expression calling MockHook() isnt evaluated until it's invoked and at that time MockHook has been initialized.

And there you have it, Jest mocks demystified. Thanks for reading and remember to . . . oh, it's not clear why those work? Are you the type to get upset when you see a library as an answer to a StackOverflow question?

Image description

Before we can dig into what Jest is doing with these mocks we need to understand a bit about how Jest and Javascript actually run code.

Hoisting in Jest is Execution Order Manipulation

The Jest docs say Jest creates mocks through hoisting. But what Jest is doing is manipulating the code execution order. The Jest testing finds those mock() calls in a file and execute them before any of the import statements. So it doesnt matter where you put your mock in a test file, Jest will hoist it above the import statements.

Image description

Hoisting in Javascript is Reference Assignment Manipulation

So while Jest actually changes the order your code runs, Javascript doesnt. Hoisting is what the Javascript engine does before executing any code at all. The Javascript engine first runs through your code to create what's called an Execution Context. The process of creating an execution context is beyond the scope of this article, but Ill review the relevant parts.

Javascript Execution Context Summary

  • The Javascript engine runs your code twice, creating an Execution Context from it and then executing it
  • The Execution Context has an identifier for every variable, function, and class
  • During the creation phase, the Javascript engine only assigns references in memory to function declarations and classes. It leaves everything else undefined
  • Function expressions differ from function declarations because function expressions are (1) undefined until you initialize them and (2) evaluated only when you invoke them

In other words, hoisting in Javascript means some variables get references to objects in memory before code execution happens.

Javascript Execution Context In-Depth

The Execution Context is a stack frame that gets created every time you call a function. During the creation stage, the Javascript engine runs through your code, grabs all the variables, functions, and classes, puts their name at the top of the scope (we'll call these names identifiers) and does things with them. What things the engine does depends on what it's looking at.

If the engine finds a class or function declaration, it takes the definition, adds it to the heap, and then points the identifier to that definition in memory. I.e. each identifier gets a reference to the definition.

If the engine finds a variable, it marks it undefined and moves on. It doesnt matter if you defined the variable with var, const, or let because the Javascript engine still leaves them undefined.

Function expressions are like variables with an extra step: the function body isnt evaluated until you invoke the function expression. Well cover this in more detail later.

After creating the execution context, the engine does a second pass on your code and evaluates it line by line. Look at the examples below to see how the Execution Context (EC) influences the output of our code:

// Variable isn't declared, so doesn't exist at all in the ECconsole.log(needsDeclaration)// Variable declared, but left undefined in the EC during creation phaseconsole.log(needsInitialization)var needsInitialization = 5;// Function defintion assigned during the creation phase, so you can call it before defining ithoistedFoo()function hoistedFoo(){...}// Function expressions are just like variables, they are undefined until initializedunhoistedBar()var unhoistedBar = () => {...}// Const and let are the same as var, but will raise an error to avoid accessing an undefined variableconsole.log(alsoNeedsInitialization)const alsoNeedsInitialization = 5

To recap, Jest hoists mock() statements by manipulating the execution order, while Javascript hoists memory allocation by manipulating when it assigns references. The tricky part is that both of these hoistings happen when you run a test: Javascript will manipulate your references during the creation phase, and then during the execution phase Jest will manipulate the order your code executes in.

Image description

Six Stages of Running A Jest Test

Stage 0: Javascript creation phase runs and sets up the Execution Context

Stage 1: Mocks are executed

  • Jest finds the jest.mock() calls and invokes those functions before the import statements occur.
  • Mocks are created during this stage, but the function expressions passed to mock() are not yet evaluated.
  • This is happening during the Javascript engines execution phase, not its creation phase.

Stage 2: Import Calls are executed

  • Jest works down the component tree of each import statement before moving on to the next.
  • If Jest finds a mock, it will import that instead of the module.
  • The factory expression you passed to mock()gets evaluated here. Whatever is in the Execution Context at this stage is what gets used for that factory expression.

Stage 3: Jest collects the blocks, but doesn't execute them

  • Jest runs through the describe and it blocks in your test, but it doesnt evaluate the function expressions you passed in.

Stage 4: Javascript executes the rest of the file

  • Whatever is defined below the describe and it blocks gets evaluated. This means the Execution Context your tests run in can be updated after the tests are written

Stage 5: Jest runs the tests

Revisiting the Examples

Now that we've covered beginning to end what happens when we run a test file, let's revisit the original examples. Ill include the code again to save you from going back up:

Ex 1: Inline mock

jest.mock("ThunderGunExpress", () => ({    useDolphLundgren: () => <></>,}));

This code raises an error because the mock will run during Stage 1 but the fragment would be imported during Stage 2. So Jest raises an error to remind you that the fragment may not exist.

Ex 2: Passing a function declaration

function MockHook(props) {    return <></>};jest.mock("ThunderGunExpress", () => ({    useDolphLundgren: MockHook,}));

This works because the function definition for MockHook gets added to memory and referenced to the MockHook identifier during Stage 0. When the mock gets created in Stage 1, Jest replaces the module with the function definition referenced by MockHook.

Ex 3: Passing a function expression

const MockHook = (props) => {    return <></>};jest.mock("ThunderGunExpress", () => ({    useDolphLundgren: MockHook,}));

This fails because Jest is hoisting the call to mock and so MockHook gets moved into the Temporal Dead Zone. The code actually executes like this:

jest.mock("ThunderGunExpress", () => ({    useDolphLundgren: MockHook,}));const MockHook = (props) => {    return <></>};

Ex 4: Calling the function expression mock within another function expression

const MockHook = (props) => {    return <></>};jest.mock("ThunderGunExpress", () => ({    useDolphLundgren: props => MockHook(props),}));

Line 5 looks like it should be identical to the same line in Example 3. But theres a difference in when the variable MockHook is accessed. The timing for when a variable is accessed is subtle enough that it deserves some more attention.

Variable Access Timing

Lets take Jest out of the equation and dig a little deeper into how function expressions get evaluated.

// needsInitializing is in the Temporal Deadzoneconst foo = {  bar: needsInitializing}const needsInitializing = () => console.log("initialized")foo.bar()

The Javascript engine checks the value of needsInitializing when attempting to assign it to bar during the creation phase. But that means needsInitializing is accessed before it is defined. Compare this to the example below:

// The expression at line 3 isn't evaluated until line 6const foo = {  bar: () => needsInitializing()}const needsInitializing = () => console.log("initialized")foo.bar()

This time, the Javascript engine accesses needsInitializing during the execution phase when foo.bar() is invoked. The difference is that during the creation phase Javascript engine doesnt attempt to evaluate the function expression in line 2 and, so, never tries to access the needsInitializing variable.

Ex 4: One Last Time

// The expression at line 6 isn't evaluated until `useDolphLundgren` is calledconst MockHook = () => {    return <></>};jest.mock("ThunderGunExpress", () => ({    useDolphLundgren: () => MockHook(),}));

Stage 0: Creation

  • MockHook is undefined,
  • (props) => MockHook(props) is unevaluated
  • MockHook is not accessed.

Stage 1: Mocks executes

  • Jest moves the execution order of jest.mock() above the const MockHook initialization to mock the useDolphLundgren module.

Stage 2: Import calls executed

Stage 3: Blocks collected

Stage 4: Rest of test file executed

  • MockHook is initialized

Stage 5: Tests Run

  • MoviePoster invokes useDolphLundgren() while rendering
  • Jest gets the mock component from useDolphLundgren
  • () => MockHook() gets evaluated
  • MockHook was defined in Stage 4 so the mock gets added.

Stage 6: Success

  • Use your Javascript skills to impress family and friends

Conclusion

Oftentimes we hit difficulties coding because we make unspoken and inaccurate assumptions. Its easy to assume that Jest and Javascript mean the same thing when they talk about hoisting. Jest, however, uses hoisting to mean manipulating the code execution order and Javascript uses hoisting to mean manipulating when objects in memory get references. I haven't covered how I tested those assumptions though. My first draft did include the steps I used to tease out the information here, but the length grew out of hand. I'll include in a follow up article the process I used to figure out the information here.

About Jobber

We're hiring for remote positions across Canada at all software engineering levels!

Our awesome Jobber technology teams span across Payments, Infrastructure, AI/ML, Business Workflows & Communications. We work on cutting edge & modern tech stacks using React, React Native, Ruby on Rails, & GraphQL.

If you want to be a part of a collaborative work culture, help small home service businesses scale and create a positive impact on our communities, then visit our careers site to learn more!


Original Link: https://dev.to/jobber/serious-jest-making-sense-of-hoisting-253i

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To