JSDOM環境を使っていると、いくつか実装されていないAPIがある。もしそのAPIを使っている実装のテストを書きたい場合、そのAPIと近い動きをする仮のオブジェクトに置き換える、つまりfakeする必要がある。
localStorageもJSDOMでは現在動かないAPIなので、localStorageを使った機能をテストするにはfakeする必要がある。普通のJSの環境だと、https://github.com/tmpvar/jsdom/issues/1137#issuecomment-173039751 に書いてあるようなオブジェクトを代入すれば良いのだけど、TypeScriptだと型定義どおりに実装しないとうまくいかないので、今回はTypeScript環境でfakeするのをやってみた。
一番単純にfakeする
方法としては同じインターフェースを持った別のものをwindow.localStorageに代入すればよい。TypeScriptの型定義lib.d.tsとかを参照すると、localStorageはStorageインターフェースを実装する必要があり、
interface WindowLocalStorage { localStorage: Storage; }
さらにStorageインターフェースを確認すると
interface Storage { length: number; clear(): void; getItem(key: string): any; key(index: number): string; removeItem(key: string): void; setItem(key: string, data: string): void; [key: string]: any; [index: number]: string; }
のようにプロパティとしてlength, 関数としてclear, getItem, key, removeItem, setItem, 他にプロパティとしてはstring -> anyか、number -> stringのものを入れられることがわかる。
この中空最低限getItem, setItem, removeItemくらいを実装してみれば、簡単なfakeはできそう。fake用のクラスを定義したのは以下のとおり。
class FakeStorage implements Storage { length: number; [key: string]: any; [index: number]: string; constructor() {} getItem(key: string): any { return this[key]; } setItem(key: string, data: string): void { this[key] = data; } removeItem(key: string): void { delete this[key]; } key(index: number): string { // not implement } clear(): void { // not implement } } export { FakeStorage };
あとはこれを使って、JSDOM環境のwindow.localStorageに代入しておけば使える。
import { FakeStorage } from "./FakeStorage"; window.localStorage = new FakeStorage(); localStorage.setItem('hoge', 'fuga'); console.log(localStorage.getItem('hoge')); // fuga localStorage.removeItem('hoge'); console.log(localStorage.getItem('hoge')); // null // 使い終わったら元に戻す window.localStorage = undefined;
これで実装でsetItem, getItem, removeItemしか使っていないなら、最低限のテストができるようになった。
他のAPIも実装する
上ので大体良いけど、興味本位でさらに他のAPIも実装してみた。実装したのは以下のとおり。
class FakeStorage implements Storage { [key: string]: any; [index: number]: string; constructor() {} get length(): number { return this._keys().length; } getItem(key: string): any { return this[key]; } setItem(key: string, data: string): void { this[key] = data; } removeItem(key: string): void { delete this[key]; } key(index: number): string { let key = this._keys()[index]; return !!key ? key : null; } clear(): void { this._keys().forEach((key) => { this.removeItem(key); }); } private _keys(): string[] { return Object.keys(this); } }
lengthをgetterで実装し、あとはこのオブジェクトのkeysを取得し実装している。
localStorageをfakeするための便利ユーティリティを用意する
このままでもやりたいことは既にできたが、もしJSDOM環境にlocalStorageが実装された場合、最後にundefinedを代入してしまうとおかしくなってしまう。もし実装されたとしても、最初にwindow.localStorageを退避しておいて、最後に退避したオブジェクトを代入すればおかしくならないはず。
そこでそのようなことを簡単にできる便利なユーティリティを作ってみた。
interface FakeLocalStorage { restore(): void; } function useFakeLocalStorage(): FakeLocalStorage { let _origLocalStorage = window.localStorage; let fakeLocalStorage = new FakeStorage(); window.localStorage = fakeLocalStorage; return { restore: function () { window.localStorage = _origLocalStorage; } } }
このuseFakeLocalStorage()は以下のように使える。
// useFakeLocalStorage()を呼び出すとwindow.localStorageが置き換わり // まっさらなlocalStorageを使っている状態になる let fake = useFakeLocalStorage(); // fakeしたものを使っていろいろ書ける localStorage.setItem('hoge', 'fuga'); console.log(localStorage.getItem('hoge')); // fuga localStorage.removeItem('hoge'); console.log(localStorage.getItem('hoge')); // null // fake.restore()を呼ぶと元の状態に戻る fake.restore();
これを使ったらもしlocalStorageがJSDOMに実装されたとしても、特定のテストで一回localStorageがまっさらな状態でテストし、その後元に戻すということを簡単にできて嬉しい。
まとめ
今回はTypeScriptでJSDOM環境のlocalStorageをfakeする方法について書いた。TypeScriptの型定義ファイルを見るとインターフェースを参照できるので、それにしたがって作るだけで良いということが分かってよかった。他にもいい方法があれば教えて下さい。
そういえば https://github.com/tmpvar/jsdom/issues/1137#issuecomment-215997211 の議論で見つけた、node.js v6を使っていると、localStorageがPROXIESされる(?)ということがよく分かっていないけど、どういうことだろう?node v6を使えばJSDOMでlocalStorage使える?