简体   繁体   中英

When mutating a module object, why are curly brace imports not changed?

I'm playing around with tidying up a code base that does some direct module mutations, rather than using any mocking behaviour, and I've noticed something odd.

If I, starting with a fresh create-react-app do something like this in a test:

import React from 'react';
import { render } from '@testing-library/react';
import App from './App';
import lodash from "lodash"; 
test('renders learn react link', () => {

  // Directly mutating the module
  React.useContext = jest.fn();
  React.foo = "foo";
  lodash.drop = jest.fn(); 

  const { getByText } = render(<App />);
  const linkElement = getByText(/learn react/i);
  expect(linkElement).toBeInTheDocument();
});

Then when I later go to use these mutations:

import React, {useContext, foo} from 'react';
import logo from './logo.svg';
import './App.css';
import lodash, {drop} from "lodash"; 
function App() {


  console.log(useContext, React.useContext, foo, React.foo );
  console.log(drop, lodash.drop); 
  // The rest doesn't matter. 

Then what we can see is that the React.useContext , React.foo , lodash.drop will print the jest mock function, whereas the useContext , foo and drop will print the original object.

Full print:

  console.log src/App.js:8
    [Function: useContext] [Function: mockConstructor] {
      _isMockFunction: true,
      getMockImplementation: [Function (anonymous)],
      mock: [Getter/Setter],
      mockClear: [Function (anonymous)],
      mockReset: [Function (anonymous)],
      mockRestore: [Function (anonymous)],
      mockReturnValueOnce: [Function (anonymous)],
      mockResolvedValueOnce: [Function (anonymous)],
      mockRejectedValueOnce: [Function (anonymous)],
      mockReturnValue: [Function (anonymous)],
      mockResolvedValue: [Function (anonymous)],
      mockRejectedValue: [Function (anonymous)],
      mockImplementationOnce: [Function (anonymous)],
      mockImplementation: [Function (anonymous)],
      mockReturnThis: [Function (anonymous)],
      mockName: [Function (anonymous)],
      getMockName: [Function (anonymous)]
    } undefined foo

  console.log src/App.js:9
    [Function: drop] [Function: mockConstructor] {
      _isMockFunction: true,
      getMockImplementation: [Function (anonymous)],
      mock: [Getter/Setter],
      mockClear: [Function (anonymous)],
      mockReset: [Function (anonymous)],
      mockRestore: [Function (anonymous)],
      mockReturnValueOnce: [Function (anonymous)],
      mockResolvedValueOnce: [Function (anonymous)],
      mockRejectedValueOnce: [Function (anonymous)],
      mockReturnValue: [Function (anonymous)],
      mockResolvedValue: [Function (anonymous)],
      mockRejectedValue: [Function (anonymous)],
      mockImplementationOnce: [Function (anonymous)],
      mockImplementation: [Function (anonymous)],
      mockReturnThis: [Function (anonymous)],
      mockName: [Function (anonymous)],
      getMockName: [Function (anonymous)]
    }

 PASS  src/App.test.js (6.875s)
  ✓ renders learn react link (304ms)


Why is this?

I assume that it's something to do with the way import/require is resolving the imports, and that doing:

import React, {useContext} from "react"

is not the same as doing

import React from "react"; 
const {useContext} = React

As counter-intuitive as that is.

This is related to When the variables/constants are created.

Import is "executed" before you mutate your object and the constant foo, is then created before you mutate React.foo.

const obj = {a: 1, b: 2};
const a = obj.a; // import
obj.a = 42; // your mock injection

console.log(a); // it will keep returning 1    

The way this is handled, is that when a ( foo or useContext in your example) is created, it points to the position of memory hold by obj.a , which contains a 1 in the moment of creation. Then obj.a points to a new position of memory where a 42 is stored, but obj.a and a are independent one to another.

In your case my recommendation is to use a Dependency Injection solution.

Either App receive a parameter for which dependency it needs, and then you mock it in the test. <App foo={myFooMock} /> or another usual solution is to sent a context, and have a context creator for Tests with mocks and a context creator for production app.

const myMockedContext = {foo: 'foo', useContext: jest.fn()};
const { getByText } = render(<App context={myMockedContext} />);

Normally you would want to have as much of your real code in your test as possible, all your business logic so that your test fail as soon as there are changes in interfaces or error in type returns or any other unexpected behaviour among your different areas of code.

But this is an ongoing discussion between Mockist and Classists in the TDD community.

HTH

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM