ฉันจะจัดการกับ localStorage ในการทดสอบตลก ๆ ได้อย่างไร?


144

ฉันได้รับ "localStorage ไม่ได้กำหนด" ในการทดสอบ Jest ที่เหมาะสม แต่มีทางเลือกอะไรบ้าง? ชนกำแพงอิฐ

คำตอบ:


142

ทางออกที่ดีจาก@chiedo

อย่างไรก็ตามเราใช้ไวยากรณ์ ES2015 และฉันรู้สึกว่ามันสะอาดกว่าเล็กน้อยในการเขียนด้วยวิธีนี้

class LocalStorageMock {
  constructor() {
    this.store = {};
  }

  clear() {
    this.store = {};
  }

  getItem(key) {
    return this.store[key] || null;
  }

  setItem(key, value) {
    this.store[key] = value.toString();
  }

  removeItem(key) {
    delete this.store[key];
  }
};

global.localStorage = new LocalStorageMock;

8
ควรทำvalue + ''ใน setter เพื่อจัดการค่า null และ undefined อย่างถูกต้อง
menehune23

ฉันคิดว่าการล้อเลียนครั้งล่าสุดเป็นเพียงการใช้สิ่งนั้น || nullนั่นคือสาเหตุที่การทดสอบของฉันล้มเหลวเพราะในการทดสอบของฉันฉันใช้not.toBeDefined()อยู่ วิธีการแก้ปัญหา @Chiedo ทำให้มันทำงานได้อีกครั้ง
jcubic

ฉันคิดว่านี่เป็นเทคนิคต้นขั้ว :) ดูที่นี่สำหรับรุ่นที่เยาะเย้ย: stackoverflow.com/questions/32911630/…
TigerBear

100

คิดออกด้วยความช่วยเหลือจาก: 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 });

จากนั้นคุณเพิ่มบรรทัดต่อไปนี้ใน package.json ภายใต้ Jest configs ของคุณ

"setupTestFrameworkScriptFile":"PATH_TO_YOUR_FILE",


6
เห็นได้ชัดว่าหนึ่งในการอัปเดตชื่อของพารามิเตอร์นี้มีการเปลี่ยนแปลงและตอนนี้มันถูกเรียกว่า "setupTestFrameworkScriptFile"
Grzegorz Pawlik

2
"setupFiles": [...]ทำงานได้เช่นกัน ด้วยตัวเลือกอาร์เรย์ให้แยก mocks เป็นไฟล์แยก เช่น:"setupFiles": ["<rootDir>/__mocks__/localStorageMock.js"]
สติกเลอ

3
ค่าส่งคืนของgetItemแตกต่างกันเล็กน้อยกับสิ่งที่จะถูกส่งกลับโดยเบราว์เซอร์หากไม่มีการตั้งค่าข้อมูลกับคีย์เฉพาะ การโทรgetItem("foo")เมื่อไม่ได้ตั้งค่าจะส่งคืนตัวอย่างเช่นnullในเบราว์เซอร์ แต่undefinedด้วยการเยาะเย้ยนี้ - นี่ทำให้การทดสอบหนึ่งของฉันล้มเหลว ทางออกที่ง่ายสำหรับฉันคือการที่จะกลับมาstore[key] || nullในgetItemฟังก์ชั่น
เบน Broadley

สิ่งนี้ใช้ไม่ได้หากคุณทำสิ่งที่ต้องการlocalStorage['test'] = '123'; localStorage.getItem('test')
rob

3
ฉันได้รับข้อผิดพลาดต่อไปนี้ - ค่า jest.fn () ต้องเป็นฟังก์ชันจำลองหรือสายลับ ความคิดใด ๆ
พอลฟิตซ์เจอรัลด์

56

หากใช้แอป create-react-app จะมีวิธีแก้ไขที่ง่ายและตรงไปตรงมาในเอกสารเอกสาร

สร้างsrc/setupTests.jsและใส่สิ่งนี้ลงไป:

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

Tom Mertz บริจาคในความคิดเห็นด้านล่าง:

จากนั้นคุณสามารถทดสอบว่าฟังก์ชั่น 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


สวัสดี c4k! คุณช่วยยกตัวอย่างวิธีที่คุณใช้ในการทดสอบได้ไหม?
Dimo

คุณหมายถึงอะไร คุณไม่ต้องเริ่มต้นอะไรในการทดสอบของคุณมันแค่ mocks โดยอัตโนมัติที่localStorageคุณใช้ในรหัสของคุณ (ถ้าคุณใช้create-react-appและสคริปต์อัตโนมัติทั้งหมดจะมีให้ตามธรรมชาติ)
c4k

จากนั้นคุณสามารถทดสอบว่าฟังก์ชั่น localStorageMock ของคุณถูกใช้โดยการทำสิ่งที่ชอบexpect(localStorage.getItem).toBeCalledWith('token')หรือexpect(localStorage.getItem.mock.calls.length).toBe(1)ภายในการทดสอบของคุณถ้าคุณต้องการตรวจสอบให้แน่ใจว่ามันถูกเรียก ลองดูfacebook.github.io/jest/docs/en/mock-functions.html
Tom Mertz

10
สำหรับสิ่งนี้ฉันได้รับข้อผิดพลาด - ค่า jest.fn () ต้องเป็นฟังก์ชันจำลองหรือสายลับ ความคิดใด ๆ
พอลฟิตซ์เจอรัลด์

3
สิ่งนี้จะไม่ทำให้เกิดปัญหาหากคุณมีการทดสอบหลายอย่างที่ใช้localStorageหรือไม่ คุณไม่ต้องการรีเซ็ตสายลับหลังจากการทดสอบแต่ละครั้งเพื่อป้องกัน "การล้น" ในการทดสอบอื่น ๆ หรือไม่?
Brandon Sturgeon

44

ในปัจจุบัน (ต.ค. 19) localStorage ไม่สามารถล้อเลียนหรือสอดแนมโดยล้อเล่นอย่างที่คุณต้องการและตามที่ระบุไว้ในเอกสารสร้างแอปที่ตอบสนอง นี่คือเนื่องจากการเปลี่ยนแปลงใน jsdom คุณสามารถอ่านเกี่ยวกับเรื่องนี้ได้ในเครื่องมือติดตามปัญหาตลกและjsdom

คุณสามารถสอดแนมต้นแบบแทน:

// does not work:
jest.spyOn(localStorage, "setItem");
localStorage.setItem = jest.fn();

// works:
jest.spyOn(window.localStorage.__proto__, 'setItem');
window.localStorage.__proto__.setItem = jest.fn();

// assertions as usual:
expect(localStorage.setItem).toHaveBeenCalled();

จริงๆแล้วมันใช้งานได้สำหรับฉันเพียงแค่มีสายลับบนไม่จำเป็นต้องแทนที่ฟังก์ชัน setItemjest.spyOn(window.localStorage.__proto__, 'setItem');
Yohan Dahmani

ใช่ฉันระบุว่าทั้งสองเป็นทางเลือกโดยไม่จำเป็นต้องทำทั้งสองอย่าง
Bastian Stein

ฉันหมายถึงโดยไม่มีการแทนที่ setItem เช่นกัน😉
Yohan Dahmani

ฉันไม่คิดว่าฉันเข้าใจ คุณช่วยอธิบายได้ไหม
Bastian Stein

1
อ่าใช่ ฉันกำลังบอกว่าคุณสามารถใช้ทั้งบรรทัดแรกหรือบรรทัดที่สอง พวกเขาเป็นทางเลือกที่ทำสิ่งเดียวกัน ไม่ว่าคุณจะชอบแบบไหน :) ขออภัยเกี่ยวกับความสับสน
Bastian Stein

14

หรือคุณเพียงแค่ใช้แพคเกจจำลองเช่นนี้:

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

มันจัดการไม่เพียง แต่ฟังก์ชั่นการจัดเก็บ แต่ยังช่วยให้คุณทดสอบว่าร้านค้าที่ถูกเรียกจริง


13

ทางเลือกที่ดีกว่าซึ่งจัดการundefinedค่า (ไม่มีtoString()) และส่งคืนnullหากไม่มีค่า ทดสอบสิ่งนี้กับreactv15 reduxและredux-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

ขอบคุณ Alexis Tyler สำหรับแนวคิดที่จะเพิ่มremoveItem: developer.mozilla.org/en-US/docs/Web/API/Storage/removeItem
Dmitriy

เชื่อว่าโมฆะและไม่ได้กำหนดต้องส่งผลให้ "โมฆะ" และ "ไม่ได้กำหนด" (สตริงตัวอักษร)
menehune23 23

6

หากคุณกำลังมองหาการเยาะเย้ยและไม่ใช่ต้นขั้วนี่คือคำตอบที่ฉันใช้:

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

ฉันส่งออกรายการเก็บข้อมูลเพื่อเริ่มต้นง่าย IE ฉันสามารถตั้งค่าเป็นวัตถุได้อย่างง่ายดาย

ในเวอร์ชันที่ใหม่กว่าของ Jest + JSDom จะไม่สามารถตั้งค่านี้ได้ แต่ LocalStorage มีอยู่แล้วและคุณสามารถสอดแนมได้เช่น:

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

5

ฉันพบโซลูชันนี้จาก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
});

คุณสามารถแทรกรหัสนี้ใน setupTests ของคุณและควรใช้งานได้ดี

ฉันทดสอบในโครงการที่มี typesctipt


สำหรับฉัน Object.defineProperty ทำเคล็ดลับ การกำหนดวัตถุโดยตรงไม่ทำงาน ขอบคุณ!
Vicens Fayos

4

น่าเสียดายที่โซลูชันที่ฉันพบที่นี่ไม่ได้ผลสำหรับฉัน

ดังนั้นฉันจึงดูปัญหา Jest GitHub และพบกระทู้นี้

โซลูชันที่มีการโหวตมากที่สุดคือโซลูชันเหล่านี้:

const spy = jest.spyOn(Storage.prototype, 'setItem');

// or

Storage.prototype.getItem = jest.fn(() => 'bla');

การทดสอบของฉันไม่มีwindowหรือStorageกำหนดไว้เช่นกัน อาจเป็นเวอร์ชั่นเก่าของ Jest ที่ฉันใช้อยู่
Antrikshy

3

ในฐานะที่เป็น @ ck4 เอกสารแนะนำมีคำอธิบายที่ชัดเจนสำหรับการใช้localStorageในการล้อเล่น อย่างไรก็ตามฟังก์ชั่นจำลองล้มเหลวในการดำเนินการlocalStorageวิธีการใด ๆ

ด้านล่างนี้เป็นตัวอย่างโดยละเอียดขององค์ประกอบการตอบสนองของฉันซึ่งใช้ประโยชน์จากวิธีนามธรรมสำหรับการเขียนและอ่านข้อมูล

//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 = {}
};

มีการอ้างอิงตัวอย่างจากที่นี่



2

Riffed คำตอบอื่น ๆ ที่นี่เพื่อแก้ปัญหาสำหรับโครงการที่มี 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 ที่ฉันใช้สำหรับการเข้าถึงที่จัดเก็บในตัวเครื่องในแอพแทนการเข้าถึงตัวแปรที่จัดเก็บในตัวเครื่องโดยตรง ทำให้ง่ายต่อการตั้งค่าจำลองในเสื้อคลุมสำหรับการทดสอบ


2
    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);

    });
});

สร้างแบบจำลองและเพิ่มลงในglobalobjectt


2

คุณต้องจำลองที่เก็บข้อมูลในเครื่องด้วยตัวอย่างนี้

// localStorage.js

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
});

และในการกำหนดค่าล้อเล่น:

"setupFiles":["localStorage.js"]

อย่าลังเลที่จะถามอะไร


1

โซลูชันต่อไปนี้สามารถใช้งานร่วมกับการทดสอบด้วยการกำหนดค่า TypeScript, ESLint, TSLint และ Prettier ที่เข้มงวดขึ้น{ "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


1

หากต้องการทำสิ่งเดียวกันใน typescript ให้ทำดังต่อไปนี้:

ตั้งค่าไฟล์ด้วยเนื้อหาต่อไปนี้:

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 });

จากนั้นคุณเพิ่มบรรทัดต่อไปนี้ใน package.json ภายใต้ Jest configs ของคุณ

"setupTestFrameworkScriptFile":"PATH_TO_YOUR_FILE",

หรือคุณนำเข้าไฟล์นี้ในกรณีทดสอบของคุณที่คุณต้องการเยาะเย้ย localstorage


โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.