如何在开玩笑的测试中处理本地存储



我在 Jest 测试中不断得到"本地存储未定义",这是有道理的,但我有什么选择?撞砖墙。

来自

@chiedo的好解决方案

但是,我们使用 ES2015 语法,我觉得这样写会更干净一些。

class LocalStorageMock {
  constructor() {
    this.store = {};
  }
  clear() {
    this.store = {};
  }
  getItem(key) {
    return this.store[key] || null;
  }
  setItem(key, value) {
    this.store[key] = String(value);
  }
  removeItem(key) {
    delete this.store[key];
  }
}
global.localStorage = new LocalStorageMock;

在以下的帮助下弄清楚了:https://groups.google.com/forum/#!topic/jestjs/9EPhuNWVYTg

设置包含以下内容的文件:

var localStorageMock = (function() {
  var store = {};
  return {
    getItem: function(key) {
      return store[key];
    },
    setItem: function(key, value) {
      store[key] = value.toString();
    },
    clear: function() {
      store = {};
    },
    removeItem: function(key) {
      delete store[key];
    }
  };
})();
Object.defineProperty(window, 'localStorage', { value: localStorageMock });

然后,您将以下行添加到 Jest 配置下的 package.json 中

"setupTestFrameworkScriptFile":"PATH_TO_YOUR_FILE",

答案

目前(22 年 7 月(本地存储不能像往常那样被开玩笑嘲笑或监视,正如创建反应应用程序文档中概述的那样。这是由于在 jsdom 中所做的更改。您可以在jest和jsdom问题跟踪器中阅读有关它的信息。

作为解决方法,您可以监视原型:

// does not work:
jest.spyOn(localStorage, "setItem");
localStorage.setItem = jest.fn();
// either of these lines will work, different syntax that does the same thing:
jest.spyOn(Storage.prototype, 'setItem');
Storage.prototype.setItem = jest.fn();
// assertions as usual:
expect(localStorage.setItem).toHaveBeenCalled();

关于监视原型的说明

监视实例使您能够观察和模拟特定对象的行为。

另一方面,监视原型将一次观察/操作该类的每个实例。除非您有特殊的用例,否则这可能不是您想要的。

虽然在许多情况下处理localStorage,使用原型是可以接受的,@onlywei指出了一个重要的警告:

此策略将不允许您区分localStoragesessionStorage,因为两者都是Storage的实例,并从存储原型继承。

对于处理这两个概念的实现,您需要考虑具体情况和影响。

如果使用create-react-app,文档中有一个更简单,更直接的解决方案。

创建src/setupTests.js并将其放入其中:

const localStorageMock = {
  getItem: jest.fn(),
  setItem: jest.fn(),
  clear: jest.fn()
};
global.localStorage = localStorageMock;

汤姆·默茨在下面的评论中的贡献:

然后,您可以通过执行以下操作来测试您的 localStorageMock 的函数是否被使用

expect(localStorage.getItem).toBeCalledWith('token')
// or
expect(localStorage.getItem.mock.calls.length).toBe(1)

在测试中,如果您想确保它被调用。查看 https://facebook.github.io/jest/docs/en/mock-functions.html

不幸的是,我在这里找到的解决方案对我不起作用。

所以我正在查看 Jest GitHub 问题并找到了这个线程

投票最多的解决方案是以下解决方案:

const spy = jest.spyOn(Storage.prototype, 'setItem');
// or
Storage.prototype.getItem = jest.fn(() => 'bla');
或者

你只需要像这样做一个模拟包:

https://www.npmjs.com/package/jest-localstorage-mock

它不仅处理存储功能,还允许您测试是否实际调用了存储。

一个更好的选择,它处理undefined值(它没有toString()(并在值不存在时返回null。使用 react v15、reduxredux-auth-wrapper 对此进行了测试

class LocalStorageMock {
  constructor() {
    this.store = {}
  }
  clear() {
    this.store = {}
  }
  getItem(key) {
    return this.store[key] || null
  }
  setItem(key, value) {
    this.store[key] = value
  }
  removeItem(key) {
    delete this.store[key]
  }
}
global.localStorage = new LocalStorageMock

如果您正在寻找模拟而不是存根,这是我使用的解决方案:

export const localStorageMock = {
   getItem: jest.fn().mockImplementation(key => localStorageItems[key]),
   setItem: jest.fn().mockImplementation((key, value) => {
       localStorageItems[key] = value;
   }),
   clear: jest.fn().mockImplementation(() => {
       localStorageItems = {};
   }),
   removeItem: jest.fn().mockImplementation((key) => {
       localStorageItems[key] = undefined;
   }),
};
export let localStorageItems = {}; // eslint-disable-line import/no-mutable-exports

我导出存储项以便于初始化。 即我可以轻松地将其设置为对象

在较新版本的Jest + JSDom中,无法设置它,但是本地存储已经可用,您可以像这样监视它:

const setItemSpy = jest.spyOn(Object.getPrototypeOf(window.localStorage), 'setItem');

对于JestReactTypeScript用户:

我创建了一个mockLocalStorage.ts

export const mockLocalStorage = () => {
  const setItemMock = jest.fn();
  const getItemMock = jest.fn();
  beforeEach(() => {
    Storage.prototype.setItem = setItemMock;
    Storage.prototype.getItem = getItemMock;
  });
  afterEach(() => {
    setItemMock.mockRestore();
    getItemMock.mockRestore();
  });
  return { setItemMock, getItemMock };
};

我的组件:

export const Component = () => {
    const foo = localStorage.getItem('foo')
    localStorage.setItem('bar', 'true')
    return <h1>{foo}</h1>
}

然后在我的测试中,我像这样使用它:

import React from 'react';
import { mockLocalStorage } from '../../test-utils';
import { Component } from './Component';
const { getItemMock, setItemMock } = mockLocalStorage();
it('fetches something from localStorage', () => {
    getItemMock.mockReturnValue('bar');
    render(<Component />);
    expect(getItemMock).toHaveBeenCalled();
    expect(getByText(/bar/i)).toBeInTheDocument()
});
it('expects something to be set in localStorage' () => {
    const value = "true"
    const key = "bar"
    render(<Component />);
    expect(setItemMock).toHaveBeenCalledWith(key, value);
}

我从 github 找到了这个解决方案

var localStorageMock = (function() {
  var store = {};
  return {
    getItem: function(key) {
        return store[key] || null;
    },
    setItem: function(key, value) {
        store[key] = value.toString();
    },
    clear: function() {
        store = {};
    }
  }; 
})();
Object.defineProperty(window, 'localStorage', {
 value: localStorageMock
});

您可以将此代码插入到安装程序测试中,它应该可以正常工作。

我用typectipt在一个项目中测试了它。

使用TypeScript和Jest的更优雅的解决方案。

    interface Spies {
      [key: string]: jest.SpyInstance
    }
    
    describe('→ Local storage', () => {
    
      const spies: Spies = {}
    
      beforeEach(() => {
        ['setItem', 'getItem', 'clear'].forEach((fn: string) => {
          const mock = jest.fn(localStorage[fn])
          spies[fn] = jest.spyOn(Storage.prototype, fn).mockImplementation(mock)
        })
      })
    
      afterEach(() => {
        Object.keys(spies).forEach((key: string) => spies[key].mockRestore())
      })
    
      test('→ setItem ...', async () => {
          localStorage.setItem( 'foo', 'bar' )
          expect(localStorage.getItem('foo')).toEqual('bar')
          expect(spies.setItem).toHaveBeenCalledTimes(1)
      })
    })

2022 年的更新。

Jest@24+ 能够自动模拟本地存储。但是,默认情况下,所需的依赖项不再随之而来。

npm i -D jest-environment-jsdom

您还需要更改 Jest 测试模式:

// jest.config.cjs
module.exports = {
  ...
  testEnvironment: "jsdom",
  ...
};

现在本地存储已经为您模拟了。

例:

// myStore.js
const saveLocally = (key, value) => {
  localStorage.setItem(key, value)
};

测试:

// myStore.spec.ts
import { saveLocally } from "./myStore.js"
it("saves key-value pair", () => {
  let key = "myKey";
  let value = "myValue";
  expect(localStorage.getItem(key)).toBe(null);
  saveLocally(key, value);
  expect(localStorage.getItem(key)).toBe(value);
};

您可以使用此方法以避免嘲笑。

Storage.prototype.getItem = jest.fn(() => expectedPayload);
Object.defineProperty(window, "localStorage", {
  value: {
    getItem: jest.fn(),
    setItem: jest.fn(),
    removeItem: jest.fn(),
  },
});

jest.spyOn(Object.getPrototypeOf(localStorage), "getItem");
jest.spyOn(Object.getPrototypeOf(localStorage), "setItem");

2021,打字稿

class LocalStorageMock {
  store: { [k: string]: string };
  length: number;
  constructor() {
    this.store = {};
    this.length = 0;
  }
  /**
   * @see https://developer.mozilla.org/en-US/docs/Web/API/Storage/key
   * @returns
   */
  key = (idx: number): string => {
    const values = Object.values(this.store);
    return values[idx];
  };
  clear() {
    this.store = {};
  }
  getItem(key: string) {
    return this.store[key] || null;
  }
  setItem(key: string, value: string) {
    this.store[key] = String(value);
  }
  removeItem(key: string) {
    delete this.store[key];
  }
}
export default LocalStorageMock;

然后,您可以将其与

global.localStorage = new LocalStorageMock();

正如@ck4建议的那样,文档对在开玩笑中使用localStorage有明确的解释。但是,模拟函数无法执行任何localStorage方法。

下面是我的 react 组件的详细示例,它使用抽象方法来写入和读取数据,

//file: storage.js
const key = 'ABC';
export function readFromStore (){
    return JSON.parse(localStorage.getItem(key));
}
export function saveToStore (value) {
    localStorage.setItem(key, JSON.stringify(value));
}
export default { readFromStore, saveToStore };

错误:

TypeError: _setupLocalStorage2.default.setItem is not a function

修复:
在下面添加模拟开玩笑功能(路径:.jest/mocks/setUpStore.js(

let mockStorage = {};
module.exports = window.localStorage = {
  setItem: (key, val) => Object.assign(mockStorage, {[key]: val}),
  getItem: (key) => mockStorage[key],
  clear: () => mockStorage = {}
};

代码段从这里引用

要在打字稿中执行相同的操作,请执行以下操作:

设置包含以下内容的文件:

let localStorageMock = (function() {
  let store = new Map()
  return {
    getItem(key: string):string {
      return store.get(key);
    },
    setItem: function(key: string, value: string) {
      store.set(key, value);
    },
    clear: function() {
      store = new Map();
    },
    removeItem: function(key: string) {
        store.delete(key)
    }
  };
})();
Object.defineProperty(window, 'localStorage', { value: localStorageMock });

然后,您将以下行添加到 Jest 配置下的 package.json 中

"setupTestFrameworkScriptFile":"PATH_TO_YOUR_FILE",

或者,您将此文件导入要模拟本地存储的测试用例中。

describe('getToken', () => {
    const Auth = new AuthService();
    const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Ik1yIEpvc2VwaCIsImlkIjoiNWQwYjk1Mzg2NTVhOTQ0ZjA0NjE5ZTA5IiwiZW1haWwiOiJ0cmV2X2pvc0Bob3RtYWlsLmNvbSIsInByb2ZpbGVVc2VybmFtZSI6Ii9tcmpvc2VwaCIsInByb2ZpbGVJbWFnZSI6Ii9Eb3Nlbi10LUdpci1sb29rLWN1dGUtbnVrZWNhdDMxNnMtMzExNzAwNDYtMTI4MC04MDAuanBnIiwiaWF0IjoxNTYyMzE4NDA0LCJleHAiOjE1OTM4NzYwMDR9.YwU15SqHMh1nO51eSa0YsOK-YLlaCx6ijceOKhZfQZc';
    beforeEach(() => {
        global.localStorage = jest.fn().mockImplementation(() => {
            return {
                getItem: jest.fn().mockReturnValue(token)
            }
        });
    });
    it('should get the token from localStorage', () => {
        const result  = Auth.getToken();
        expect(result).toEqual(token);
    });
});

创建模拟并将其添加到global对象

至少到目前为止,localStorage可以很容易地在你的玩笑测试中被监视,例如:

const spyRemoveItem = jest.spyOn(window.localStorage, 'removeItem')

仅此而已。您可以像以前一样使用间谍。

正如Niket Pathak的评论中提到的,从jest@24/jsdom@11.12.0 及更高版本开始,localStorage会自动模拟。

这对我有用,只有一个代码行

const setItem = jest.spyOn(Object.getPrototypeOf(localStorage), 'setItem');

在这里

提出了一些其他答案,以解决带有Typescript的项目。我创建了一个像这样的LocalStorageMock:

export class LocalStorageMock {
    private store = {}
    clear() {
        this.store = {}
    }
    getItem(key: string) {
        return this.store[key] || null
    }
    setItem(key: string, value: string) {
        this.store[key] = value
    }
    removeItem(key: string) {
        delete this.store[key]
    }
}

然后,我创建了一个 LocalStorageWrapper 类,用于对应用程序中的本地存储的所有访问,而不是直接访问全局本地存储变量。可以轻松地在包装器中设置模拟进行测试。

以下解决方案兼容使用更严格的 TypeScript、ESLint、TSLint 和更漂亮的配置进行测试: { "proseWrap": "always", "semi": false, "singleQuote": true, "trailingComma": "es5" }

class LocalStorageMock {
  public store: {
    [key: string]: string
  }
  constructor() {
    this.store = {}
  }
  public clear() {
    this.store = {}
  }
  public getItem(key: string) {
    return this.store[key] || undefined
  }
  public setItem(key: string, value: string) {
    this.store[key] = value.toString()
  }
  public removeItem(key: string) {
    delete this.store[key]
  }
}
/* tslint:disable-next-line:no-any */
;(global as any).localStorage = new LocalStorageMock()

HT/https://stackoverflow.com/a/51583401/101290 了解如何更新 global.localStorage

没有必要

模拟 localStorage - 只需使用 jsdom 环境,以便您的测试在类似浏览器的条件下运行。

在你的jest.config.js,

module.exports = {
    // ...
    testEnvironment: "jsdom"
}
    

上面的答案都不适合我。所以经过一番挖掘,这就是我开始的工作。功劳也归功于一些来源和其他答案。

  • https://www.codeblocq.com/2021/01/Jest-Mock-Local-Storage/
  • https://github.com/facebook/jest/issues/6798#issuecomment-440988627
  • https://gist.github.com/mayank23/7b994385eb030f1efb7075c4f1f6ac4c
  • https://github.com/facebook/jest/issues/6798#issuecomment-514266034

我的全部要点:https://gist.github.com/ar-to/01fa07f2c03e7c1b2cfe6b8c612d4c6b

/**
 * Build Local Storage object
 * @see https://www.codeblocq.com/2021/01/Jest-Mock-Local-Storage/ for source
 * @see https://stackoverflow.com/a/32911774/9270352 for source
 * @returns
 */
export const fakeLocalStorage = () => {
  let store: { [key: string]: string } = {}
  return {
    getItem: function (key: string) {
      return store[key] || null
    },
    setItem: function (key: string, value: string) {
      store[key] = value.toString()
    },
    removeItem: function (key: string) {
      delete store[key]
    },
    clear: function () {
      store = {}
    },
  }
}
/**
 * Mock window properties for testing
 * @see https://gist.github.com/mayank23/7b994385eb030f1efb7075c4f1f6ac4c for source
 * @see https://github.com/facebook/jest/issues/6798#issuecomment-514266034 for sample implementation
 * @see https://developer.mozilla.org/en-US/docs/Web/API/Window#properties for window properties
 * @param { string } property window property string but set to any due to some warnings
 * @param { Object } value for property
 *
 * @example
 *
 *  const testLS = {
 *    id: 5,
 *    name: 'My Test',
 *  }
 * mockWindowProperty('localStorage', fakeLocalStorage())
 * window.localStorage.setItem('currentPage', JSON.stringify(testLS))
 *
 */
const mockWindowProperty = (property: string | any, value: any) => {
  const { [property]: originalProperty } = window
  delete window[property]
  beforeAll(() => {
    Object.defineProperty(window, property, {
      configurable: true,
      writable: true,
      value,
    })
  })
  afterAll(() => {
    window[property] = originalProperty
  })
}
export default mockWindowProperty

就我而言,我需要在检查之前设置 localStorage 值。

所以我所做的是

const data = { .......}
const setLocalStorageValue = (name: string, value: any) => {
  localStorage.setItem(name, JSON.stringify(value))
}

 describe('Check X class', () => {
  setLocalStorageValue('Xname', data)
  const xClass= new XClass()
  console.log(xClass.initiate()) ; // it will work 
  
})

2022 年 12 月:Nx 14 与 Angular 14 开玩笑。我们在每个应用程序和库文件夹中都有一个test-setup.ts文件。我们设置本地存储模拟全局。

import 'jest-preset-angular/setup-jest';
Storage.prototype.getItem = jest.fn();
Storage.prototype.setItem = jest.fn();
Storage.prototype.removeItem = jest.fn();

然后localStorage.service.spec.ts文件如下所示:

import { LocalStorageService } from './localstorage.service';
describe('LocalStorageService', () => {
    let localStorageService: LocalStorageService;
    beforeEach(() => {
        localStorageService = new LocalStorageService();
    });
    it('should set "identityKey" in localStorage', async () => {
        localStorageService.saveData('identityKey', '99');
        expect(window.localStorage.setItem).toHaveBeenCalled();
 expect(window.localStorage.setItem).toHaveBeenCalledWith('identityKey', '99');
        expect(window.localStorage.setItem).toHaveBeenCalledTimes(1);
});
it('should get "identityKey" from localStorage', async () => {
    localStorageService.getData('identityKey');
    expect(window.localStorage.getItem).toHaveBeenCalled();
    expect(window.localStorage.getItem).toHaveBeenCalledWith('identityKey');
    expect(window.localStorage.getItem).toHaveBeenCalledTimes(1);
});
it('should remove "identityKey" from localStorage', async () => {
    localStorageService.removeData('identityKey');
    expect(window.localStorage.removeItem).toHaveBeenCalled();
expect(window.localStorage.removeItem).toHaveBeenCalledWith('identityKey');
            expect(window.localStorage.removeItem).toHaveBeenCalledTimes(1);
        });
    });

附言对不起,缩进不好,这个卫星溢出窗口s*cks。

首先:我创建了一个名为localStorage.ts(localStorage.js(的文件

class LocalStorageMock {
  store: Store;
  length: number;
  constructor() {
    this.store = {};
    this.length = 0;
  }
  key(n: number): any {
    if (typeof n === 'undefined') {
      throw new Error(
        "Uncaught TypeError: Failed to execute 'key' on 'Storage': 1 argument required, but only 0 present."
      );
    }
    if (n >= Object.keys(this.store).length) {
      return null;
    }
    return Object.keys(this.store)[n];
  }
  getItem(key: string): Store | null {
    if (!Object.keys(this.store).includes(key)) {
      return null;
    }
    return this.store[key];
  }
  setItem(key: string, value: any): undefined {
    if (typeof key === 'undefined' && typeof value === 'undefined') {
      throw new Error(
        "Uncaught TypeError: Failed to execute 'setItem' on 'Storage': 2 arguments required, but only 0 present."
      );
    }
    if (typeof value === 'undefined') {
      throw new Error(
        "Uncaught TypeError: Failed to execute 'setItem' on 'Storage': 2 arguments required, but only 1 present."
      );
    }
    if (!key) return undefined;
    this.store[key] = value.toString() || '';
    this.length = Object.keys(this.store).length;
    return undefined;
  }
  removeItem(key: string): undefined {
    if (typeof key === 'undefined') {
      throw new Error(
        "Uncaught TypeError: Failed to execute 'removeItem' on 'Storage': 1 argument required, but only 0 present."
      );
    }
    delete this.store[key];
    this.length = Object.keys(this.store).length;
    return undefined;
  }
  clear(): undefined {
    this.store = {};
    this.length = 0;
    return undefined;
  }
}
export const getLocalStorageMock = (): any => {
  return new LocalStorageMock();
};
global.localStorage = new LocalStorageMock();

然后创建一个名为 session.test.ts(session.test.js( 的测试文件

import { getLocalStorageMock } from '../localstorage';
describe('session storage', () => {
  let localStorage;
  beforeEach(() => {
    localStorage = getLocalStorageMock();
  });
  describe('getItem', () => {
    it('should return null if the item is undefined', () => {
      expect(localStorage.getItem('item')).toBeNull();
    });
    it("should return '' instead of null", () => {
      localStorage.setItem('item', '');
      expect(localStorage.getItem('item')).toBe('');
    });
    it('should return navid', () => {
      localStorage.setItem('item', 'navid');
      expect(localStorage.getItem('item')).toBe('navid');
    });
  });
});

从 2023 年和 jest 29 开始,您只需通过以下方式添加 jest-environment-jsdom 包

npm install --save-dev jest-environment-jsdom

并通过以下方式激活它package.json

"jest": { "testEnvironment": "jsdom"}

现在您可以使用window.localStorage.setItem()window.localStorage.getItem()等。

这对

我有用,

delete global.localStorage;
global.localStorage = {
getItem: () => 
 }

相关内容

  • 没有找到相关文章

最新更新