วิธีการจำลอง localStorage ในการทดสอบหน่วย JavaScript


106

มีห้องสมุดไว้ล้อเลียนlocalStorageบ้างไหม?

ฉันใช้Sinon.JSสำหรับการเยาะเย้ยจาวาสคริปต์อื่น ๆ ส่วนใหญ่และพบว่ามันยอดเยี่ยมจริงๆ

การทดสอบครั้งแรกของฉันแสดงให้เห็นว่า localStorage ปฏิเสธที่จะกำหนดให้ใน firefox (sadface) ดังนั้นฉันอาจต้องการแฮ็คบางอย่างเกี่ยวกับสิ่งนี้: /

ตัวเลือกของฉัน ณ ตอนนี้ (ตามที่ฉันเห็น) มีดังนี้:

  1. สร้างฟังก์ชันการตัดที่รหัสทั้งหมดของฉันใช้และล้อเลียน
  2. สร้างการจัดการสถานะ (อาจซับซ้อน) บางประเภท (สแน็ปช็อต localStorage ก่อนการทดสอบในการล้างข้อมูลการคืนค่าสแน็ปช็อต) สำหรับ localStorage
  3. ??????

คุณคิดอย่างไรกับแนวทางเหล่านี้และคุณคิดว่ามีวิธีอื่นที่ดีกว่าในการดำเนินการนี้หรือไม่? ไม่ว่าจะด้วยวิธีใดฉันจะใส่ "ไลบรารี" ที่เป็นผลลัพธ์ที่ฉันสร้างไว้ใน github เพื่อความดีงามของโอเพ่นซอร์ส


35
คุณพลาด # 4:Profit!
Chris Laplante

คำตอบ:


133

นี่คือวิธีง่ายๆในการล้อเลียนกับจัสมิน:

beforeEach(function () {
  var store = {};

  spyOn(localStorage, 'getItem').andCallFake(function (key) {
    return store[key];
  });
  spyOn(localStorage, 'setItem').andCallFake(function (key, value) {
    return store[key] = value + '';
  });
  spyOn(localStorage, 'clear').andCallFake(function () {
      store = {};
  });
});

หากคุณต้องการจำลองพื้นที่จัดเก็บในตัวเครื่องในการทดสอบทั้งหมดของคุณให้ประกาศbeforeEach()ฟังก์ชันที่แสดงด้านบนในขอบเขตการทดสอบของคุณ (ตำแหน่งปกติคือสคริปต์specHelper.js )


1
+1 - คุณสามารถทำสิ่งนี้กับ sinon ได้เช่นกัน ที่สำคัญคือทำไมรำคาญคาดการเยาะเย้ยวัตถุ localStorage ทั้งหมดเพียงแค่วิธีการเยาะเย้ย (GetItem และ / หรือ setItem) คุณมีความสนใจใน.
s1mm0t

6
โปรดทราบ: ดูเหมือนว่าจะมีปัญหากับโซลูชันนี้ใน Firefox: github.com/pivotal/jasmine/issues/299
cthulhu

4
ฉันได้รับReferenceError: localStorage is not defined(กำลังทำการทดสอบโดยใช้ FB Jest และ npm) ... มีแนวคิดอย่างไรในการแก้ไขปัญหา?
FeifanZ

1
ลองสอดแนม window.localStorage
Benj

24
andCallFakeเปลี่ยนเป็นand.callFakeดอกมะลิ 2 +
Venugopal

51

เพียงแค่ล้อเลียน localStorage / sessionStorage ระดับโลก (มี API เดียวกัน) สำหรับความต้องการของคุณ
ตัวอย่างเช่น:

 // Storage Mock
  function storageMock() {
    let storage = {};

    return {
      setItem: function(key, value) {
        storage[key] = value || '';
      },
      getItem: function(key) {
        return key in storage ? storage[key] : null;
      },
      removeItem: function(key) {
        delete storage[key];
      },
      get length() {
        return Object.keys(storage).length;
      },
      key: function(i) {
        const keys = Object.keys(storage);
        return keys[i] || null;
      }
    };
  }

แล้วสิ่งที่คุณทำจริงๆก็เป็นอย่างนั้น:

// mock the localStorage
window.localStorage = storageMock();
// mock the sessionStorage
window.sessionStorage = storageMock();

1
แก้ไขข้อเสนอแนะ: getItemต้องส่งคืนnullเมื่อไม่มีค่า: return storage[key] || null;;
cyberwombat

8
ในปี 2016 ดูเหมือนว่าจะใช้ไม่ได้กับเบราว์เซอร์สมัยใหม่ (ตรวจสอบ Chrome และ Firefox) การลบล้างlocalStorageโดยรวมเป็นไปไม่ได้
jakub.g

2
ใช่น่าเสียดายที่มันใช้ไม่ได้อีกต่อไป แต่ฉันก็ยืนยันว่าstorage[key] || nullไม่ถูกต้อง ถ้าstorage[key] === 0จะคืนnullแทน. ฉันคิดว่าคุณสามารถทำreturn key in storage ? storage[key] : nullแม้ว่า
redbmk

เพิ่งใช้สิ่งนี้กับ SO! ทำงานได้อย่างมีเสน่ห์ - เพียงแค่ต้องเปลี่ยน localStor กลับเป็น localStorage เมื่ออยู่บนเซิร์ฟเวอร์จริงfunction storageMock() { var storage = {}; return { setItem: function(key, value) { storage[key] = value || ''; }, getItem: function(key) { return key in storage ? storage[key] : null; }, removeItem: function(key) { delete storage[key]; }, get length() { return Object.keys(storage).length; }, key: function(i) { var keys = Object.keys(storage); return keys[i] || null; } }; } window.localStor = storageMock();
mplungjan

2
@ a8m ฉันได้รับข้อผิดพลาดหลังจากอัปเดตโหนดเป็น 10.15.1 TypeError: Cannot set property localStorage of #<Window> which has only a getterแล้วฉันจะแก้ไขได้อย่างไร
Tasawer Nawaz

20

พิจารณาตัวเลือกในการฉีดการอ้างอิงในฟังก์ชันตัวสร้างของวัตถุ

var SomeObject(storage) {
  this.storge = storage || window.localStorage;
  // ...
}

SomeObject.prototype.doSomeStorageRelatedStuff = function() {
  var myValue = this.storage.getItem('myKey');
  // ...
}

// In src
var myObj = new SomeObject();

// In test
var myObj = new SomeObject(mockStorage)

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

เนื่องจากเห็นได้ชัดว่าไม่น่าเชื่อถือในการแทนที่เมธอดบนอ็อบเจ็กต์ localStorage จริงให้ใช้ mockStorage แบบ "โง่" และทำให้วิธีการแต่ละอย่างถูกตัดออกตามต้องการเช่น:

var mockStorage = {
  setItem: function() {},
  removeItem: function() {},
  key: function() {},
  getItem: function() {},
  removeItem: function() {},
  length: 0
};

// Then in test that needs to know if and how setItem was called
sinon.stub(mockStorage, 'setItem');
var myObj = new SomeObject(mockStorage);

myObj.doSomeStorageRelatedStuff();
expect(mockStorage.setItem).toHaveBeenCalledWith('myKey');

1
ฉันรู้ว่ามันเป็นเวลานานแล้วที่ฉันได้ดูคำถามนี้ - แต่ในความเป็นจริงฉันทำอะไรลงไป
Anthony Sottile

1
นี่เป็นทางออกเดียวที่คุ้มค่าเนื่องจากไม่มีความเสี่ยงสูงที่จะพังทันเวลา
oligofren

14

นี่คือสิ่งที่ฉันทำ ...

var mock = (function() {
  var store = {};
  return {
    getItem: function(key) {
      return store[key];
    },
    setItem: function(key, value) {
      store[key] = value.toString();
    },
    clear: function() {
      store = {};
    }
  };
})();

Object.defineProperty(window, 'localStorage', { 
  value: mock,
});

13

โซลูชันปัจจุบันจะไม่ทำงานใน Firefox เนื่องจาก localStorage ถูกกำหนดโดยข้อมูลจำเพาะ html ว่าไม่สามารถแก้ไขได้ อย่างไรก็ตามคุณสามารถแก้ไขปัญหานี้ได้โดยเข้าถึงต้นแบบของ localStorage โดยตรง

วิธีแก้ปัญหาเบราว์เซอร์ข้ามคือการล้อเลียนวัตถุStorage.prototypeเช่น

แทนการspyOn (localStorage 'setItem')การใช้งาน

spyOn(Storage.prototype, 'setItem')
spyOn(Storage.prototype, 'getItem')

นำมาจากคำตอบของbzbarskyและ teogeosที่นี่https://github.com/jasmine/jasmine/issues/299


1
ความคิดเห็นของคุณควรได้รับการชอบมากขึ้น ขอบคุณ!
LorisBachert

6

มีห้องสมุดไว้ล้อเลียนlocalStorageบ้างไหม?

ฉันเพิ่งเขียนหนึ่ง:

(function () {
    var localStorage = {};
    localStorage.setItem = function (key, val) {
         this[key] = val + '';
    }
    localStorage.getItem = function (key) {
        return this[key];
    }
    Object.defineProperty(localStorage, 'length', {
        get: function () { return Object.keys(this).length - 2; }
    });

    // Your tests here

})();

การทดสอบครั้งแรกของฉันแสดงให้เห็นว่า localStorage ปฏิเสธที่จะกำหนดใน firefox

เฉพาะในบริบทส่วนกลาง ด้วยฟังก์ชั่น Wrapper ตามด้านบนมันก็ใช้ได้ดี


1
คุณยังสามารถใช้ได้var window = { localStorage: ... }
user123444555621

1
น่าเสียดายที่นั่นหมายความว่าฉันจำเป็นต้องรู้ทุกคุณสมบัติที่ฉันต้องการและได้เพิ่มลงในหน้าต่างออบเจ็กต์ (และฉันพลาดต้นแบบ ฯลฯ ) รวมถึงสิ่งที่ jQuery อาจต้องการ น่าเสียดายที่ดูเหมือนจะไม่ใช่วิธีแก้ปัญหา นอกจากนี้การทดสอบเป็นการทดสอบโค้ดที่ใช้localStorageการทดสอบไม่จำเป็นต้องมีlocalStorageโดยตรง โซลูชันนี้ไม่ได้เปลี่ยนlocalStorageสคริปต์สำหรับสคริปต์อื่นดังนั้นจึงไม่ใช่โซลูชัน +1 สำหรับเคล็ดลับการกำหนดขอบเขต
Anthony Sottile

1
คุณอาจต้องปรับเปลี่ยนโค้ดของคุณเพื่อให้สามารถทดสอบได้ ฉันรู้ว่าสิ่งนี้น่ารำคาญมากและนั่นเป็นเหตุผลว่าทำไมฉันถึงชอบการทดสอบซีลีเนียมแบบหนักมากกว่าการทดสอบหน่วย
user123444555621

นี่ไม่ใช่วิธีแก้ปัญหาที่ถูกต้อง หากคุณเรียกใช้ฟังก์ชันใด ๆ จากภายในฟังก์ชันที่ไม่ระบุตัวตนนั้นคุณจะสูญเสียการอ้างอิงไปยังหน้าต่างจำลองหรือจำลองวัตถุ localStorage จุดประสงค์ของการทดสอบหน่วยคือคุณเรียกใช้ฟังก์ชันภายนอก ดังนั้นเมื่อคุณเรียกใช้ฟังก์ชันของคุณที่ทำงานกับ localStorage มันจะไม่ใช้การจำลอง แต่คุณต้องห่อรหัสที่คุณกำลังทดสอบในฟังก์ชันที่ไม่ระบุตัวตน เพื่อให้สามารถทดสอบได้ให้ยอมรับวัตถุหน้าต่างเป็นพารามิเตอร์
John Kurlak

การจำลองนั้นมีจุดบกพร่อง: เมื่อดึงรายการที่ไม่มีอยู่ getItem ควรส่งคืนค่าว่าง ในการเยาะเย้ยจะส่งกลับที่ไม่ได้กำหนด รหัสที่ถูกต้องควรเป็นif this.hasOwnProperty(key) return this[key] else return null
Evan

4

นี่คือตัวอย่างการใช้สายลับ sinon และล้อเลียน:

// window.localStorage.setItem
var spy = sinon.spy(window.localStorage, "setItem");

// You can use this in your assertions
spy.calledWith(aKey, aValue)

// Reset localStorage.setItem method    
spy.reset();



// window.localStorage.getItem
var stub = sinon.stub(window.localStorage, "getItem");
stub.returns(aValue);

// You can use this in your assertions
stub.calledWith(aKey)

// Reset localStorage.getItem method
stub.reset();

4

การเขียนทับlocalStorageคุณสมบัติของwindowอ็อบเจ็กต์ส่วนกลางตามที่แนะนำในคำตอบบางคำจะใช้ไม่ได้ในเอ็นจิน JS ส่วนใหญ่เนื่องจากประกาศlocalStorageคุณสมบัติข้อมูลว่าไม่สามารถเขียนได้และไม่สามารถกำหนดค่าได้

อย่างไรก็ตามฉันพบว่าอย่างน้อยด้วยเวอร์ชัน WebKit ของ PhantomJS (เวอร์ชัน 1.9.8) คุณสามารถใช้ API ดั้งเดิม__defineGetter__เพื่อควบคุมสิ่งที่จะเกิดขึ้นหากlocalStorageมีการเข้าถึง ยังคงน่าสนใจหากใช้งานได้ในเบราว์เซอร์อื่นด้วย

var tmpStorage = window.localStorage;

// replace local storage
window.__defineGetter__('localStorage', function () {
    throw new Error("localStorage not available");
    // you could also return some other object here as a mock
});

// do your tests here    

// restore old getter to actual local storage
window.__defineGetter__('localStorage',
                        function () { return tmpStorage });

ประโยชน์ของวิธีนี้คือคุณไม่ต้องแก้ไขโค้ดที่คุณกำลังจะทดสอบ


เพิ่งสังเกตว่าสิ่งนี้ใช้ไม่ได้ใน PhantomJS 2.1.1 ;)
Conrad Calmez

4

คุณไม่จำเป็นต้องส่งผ่านวัตถุจัดเก็บไปยังแต่ละวิธีที่ใช้ แต่คุณสามารถใช้พารามิเตอร์คอนฟิกูเรชันสำหรับโมดูลใด ๆ ที่สัมผัสอะแด็ปเตอร์จัดเก็บข้อมูล

โมดูลเก่าของคุณ

// hard to test !
export const someFunction (x) {
  window.localStorage.setItem('foo', x)
}

// hard to test !
export const anotherFunction () {
  return window.localStorage.getItem('foo')
}

โมดูลใหม่ของคุณพร้อมฟังก์ชัน config "wrapper"

export default function (storage) {
  return {
    someFunction (x) {
      storage.setItem('foo', x)
    }
    anotherFunction () {
      storage.getItem('foo')
    }
  }
}

เมื่อคุณใช้โมดูลในการทดสอบโค้ด

// import mock storage adapater
const MockStorage = require('./mock-storage')

// create a new mock storage instance
const mock = new MockStorage()

// pass mock storage instance as configuration argument to your module
const myModule = require('./my-module')(mock)

// reset before each test
beforeEach(function() {
  mock.clear()
})

// your tests
it('should set foo', function() {
  myModule.someFunction('bar')
  assert.equal(mock.getItem('foo'), 'bar')
})

it('should get foo', function() {
  mock.setItem('foo', 'bar')
  assert.equal(myModule.anotherFunction(), 'bar')
})

MockStorageชั้นอาจจะมีลักษณะเช่นนี้

export default class MockStorage {
  constructor () {
    this.storage = new Map()
  }
  setItem (key, value) {
    this.storage.set(key, value)
  }
  getItem (key) {
    return this.storage.get(key)
  }
  removeItem (key) {
    this.storage.delete(key)
  }
  clear () {
    this.constructor()
  }
}

เมื่อใช้โมดูลของคุณในรหัสการผลิตให้ส่งผ่านอะแด็ปเตอร์ localStorage จริงแทน

const myModule = require('./my-module')(window.localStorage)

fyi ถึงคนสิ่งนี้ใช้ได้เฉพาะใน es6: developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… (แต่เป็นวิธีแก้ปัญหาที่ยอดเยี่ยมและฉันไม่สามารถรอจนกว่ามันจะพร้อมใช้งานทุกที่!)
Alex Moore- Niemi

@ AlexMoore-Niemi มีการใช้ ES6 น้อยมากที่นี่ ทั้งหมดนี้สามารถทำได้โดยใช้ ES5 หรือต่ำกว่าโดยมีการเปลี่ยนแปลงน้อยมาก
ขอบคุณ

ใช่แค่ชี้export default functionและเริ่มต้นโมดูลด้วยอาร์กิวเมนต์เช่นนั้นคือ es6 เท่านั้น รูปแบบยืนโดยไม่คำนึงถึง
Alex Moore-Niemi

ฮะ? ฉันต้องใช้สไตล์เก่าrequireเพื่อนำเข้าโมดูลและใช้กับอาร์กิวเมนต์ในนิพจน์เดียวกัน ไม่มีทางทำเช่นนั้นใน ES6 ที่ฉันรู้จัก ไม่งั้นฉันคงใช้ ES6 ไปแล้วimport
ขอบคุณ

2

ฉันตัดสินใจที่จะย้ำความคิดเห็นของฉันต่อคำตอบของ Pumbaa80 เป็นคำตอบแยกต่างหากเพื่อที่จะนำกลับมาใช้เป็นห้องสมุดได้ง่ายขึ้น

ฉันใช้รหัสของ Pumbaa80 ปรับแต่งเล็กน้อยเพิ่มการทดสอบและเผยแพร่เป็นโมดูล npm ที่นี่: https://www.npmjs.com/package/mock-local-storage https://www.npmjs.com/package/mock-local-storage

นี่คือซอร์สโค้ด: https://github.com/letsrock-today/mock-local-storage/blob/master/src/mock-localstorage.js

การทดสอบบางส่วน: https://github.com/letsrock-today/mock-local-storage/blob/master/test/mock-localstorage.js

โมดูลสร้างการจำลอง localStorage และ sessionStorage บนวัตถุส่วนกลาง (หน้าต่างหรือส่วนกลางซึ่งกำหนดไว้)

ในการทดสอบโครงการอื่น ๆ ของฉันฉันกำหนดให้ใช้มอคค่าเช่นนี้mocha -r mock-local-storageเพื่อให้คำจำกัดความทั่วโลกพร้อมใช้งานสำหรับโค้ดทั้งหมดที่อยู่ระหว่างการทดสอบ

โดยทั่วไปโค้ดมีลักษณะดังนี้:

(function (glob) {

    function createStorage() {
        let s = {},
            noopCallback = () => {},
            _itemInsertionCallback = noopCallback;

        Object.defineProperty(s, 'setItem', {
            get: () => {
                return (k, v) => {
                    k = k + '';
                    _itemInsertionCallback(s.length);
                    s[k] = v + '';
                };
            }
        });
        Object.defineProperty(s, 'getItem', {
            // ...
        });
        Object.defineProperty(s, 'removeItem', {
            // ...
        });
        Object.defineProperty(s, 'clear', {
            // ...
        });
        Object.defineProperty(s, 'length', {
            get: () => {
                return Object.keys(s).length;
            }
        });
        Object.defineProperty(s, "key", {
            // ...
        });
        Object.defineProperty(s, 'itemInsertionCallback', {
            get: () => {
                return _itemInsertionCallback;
            },
            set: v => {
                if (!v || typeof v != 'function') {
                    v = noopCallback;
                }
                _itemInsertionCallback = v;
            }
        });
        return s;
    }

    glob.localStorage = createStorage();
    glob.sessionStorage = createStorage();
}(typeof window !== 'undefined' ? window : global));

โปรดทราบว่าวิธีการทั้งหมดที่เพิ่มเข้ามาObject.definePropertyจะไม่ถูกทำซ้ำเข้าถึงหรือลบออกเป็นรายการปกติและจะไม่นับความยาว นอกจากนี้ฉันยังเพิ่มวิธีการลงทะเบียนการโทรกลับซึ่งเรียกว่าเมื่อรายการกำลังจะถูกใส่ลงในวัตถุ การเรียกกลับนี้อาจใช้เพื่อเลียนแบบข้อผิดพลาดที่เกินโควต้าในการทดสอบ


2

ฉันพบว่าฉันไม่จำเป็นต้องล้อเลียนมัน ฉันสามารถเปลี่ยนที่เก็บข้อมูลในเครื่องจริงให้เป็นสถานะที่ฉันต้องการได้setItemจากนั้นเพียงแค่สอบถามค่าเพื่อดูว่ามีการเปลี่ยนแปลงgetItemหรือไม่ มันไม่ได้มีพลังมากเท่ากับการเยาะเย้ยเนื่องจากคุณไม่สามารถมองเห็นได้ว่ามีการเปลี่ยนแปลงกี่ครั้ง แต่มันก็ใช้ได้ตามวัตถุประสงค์ของฉัน


0

น่าเสียดายที่วิธีเดียวที่เราสามารถจำลองวัตถุ localStorage ในสถานการณ์ทดสอบคือการเปลี่ยนรหัสที่เรากำลังทดสอบ คุณต้องห่อรหัสของคุณในฟังก์ชันที่ไม่ระบุตัวตน (ซึ่งคุณควรทำต่อไป) และใช้ "การฉีดขึ้นต่อกัน" เพื่อส่งผ่านการอ้างอิงไปยังวัตถุหน้าต่าง สิ่งที่ต้องการ:

(function (window) {
   // Your code
}(window.mockWindow || window));

จากนั้นภายในการทดสอบของคุณคุณสามารถระบุ:

window.mockWindow = { localStorage: { ... } };

0

นี่คือวิธีที่ฉันชอบทำ ทำให้มันง่าย

  let localStoreMock: any = {};

  beforeEach(() => {

    angular.mock.module('yourApp');

    angular.mock.module(function ($provide: any) {

      $provide.service('localStorageService', function () {
        this.get = (key: any) => localStoreMock[key];
        this.set = (key: any, value: any) => localStoreMock[key] = value;
      });

    });
  });

0

ให้เครดิต https://medium.com/@armno/til-mocking-localstorage-and-sessionstorage-in-angular-unit-tests-a765abdc9d87 สร้าง localstorage ปลอมและสอดแนมใน localstorage เมื่อมันเป็น caleld

 beforeAll( () => {
    let store = {};
    const mockLocalStorage = {
      getItem: (key: string): string => {
        return key in store ? store[key] : null;
      },
      setItem: (key: string, value: string) => {
        store[key] = `${value}`;
      },
      removeItem: (key: string) => {
        delete store[key];
      },
      clear: () => {
        store = {};
      }
    };

    spyOn(localStorage, 'getItem')
      .and.callFake(mockLocalStorage.getItem);
    spyOn(localStorage, 'setItem')
      .and.callFake(mockLocalStorage.setItem);
    spyOn(localStorage, 'removeItem')
      .and.callFake(mockLocalStorage.removeItem);
    spyOn(localStorage, 'clear')
      .and.callFake(mockLocalStorage.clear);
  })

และที่นี่เราใช้มัน

it('providing search value should return matched item', () => {
    localStorage.setItem('defaultLanguage', 'en-US');

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