I have a StorageService
wrapper for @ionic/storage-angular
where I have a function that looks roughly like this:
import { Injectable } from '@angular/core';
import { Storage } from '@ionic/storage-angular';
@Injectable({
providedIn: 'root'
})
export class StorageService {
constructor(public storage: Storage) {}
async getData() {
const data = [];
await this.initStorage();
return new Promise((resolve) => {
this.storage
.forEach((value, key) => {
if (key.startsWith('data-')) {
data.push({
key: 'someKey,
value,
});
}
})
.then(() => resolve(data));
});
}
// other functions here
}
I have a unit test that looks like this
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { fakeAsync, flush, TestBed } from '@angular/core/testing';
import { StorageService } from './storage.service';
import { Storage } from '@ionic/storage-angular';
describe('StorageService', () => {
let service: StorageService;
let mockStorage: jasmine.SpyObj<Storage>;
beforeEach(() => {
mockStorage = jasmine.createSpyObj('Storage', ['get', 'set', 'remove', 'create']);
TestBed.configureTestingModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA],
providers: [
{ provide: Storage, useValue: mockStorage },
],
});
service = TestBed.inject(StorageService);
});
describe('getData()', () => {
it('should call StorageService.initStorage()', fakeAsync(() => {
spyOn<any>(service, 'initStorage').and.stub();
service.getData();
flush();
expect(service['initStorage']).toHaveBeenCalled();
}));
});
// other tests here
});
It fails with
Error: Uncaught (in promise): TypeError: _this.storage.forEach is not a function
TypeError: _this.storage.forEach is not a function
Storage itself is supposed to be iterable yet jasmine doesn't see it as such because of the way I mocked it, I assume.
The question is: how do I unit test this bit correctly? I don't want to create a real storage instance for testing purposes, is there a way to define this.storage as iterable via mocks?
Answering my own questions again.
My goal was to both preserve my spy object to mock storage.get
and storage.set
and be able to test storage.forEach
. That doesn't seem to be possible. The answer above may suit those who only need to test storage.forEach
.
I resorted to not using storage.forEach
at all and instead I iterate through storage.keys
like this:
async getData(): Promise<Array<{key: string, value}>> {
await this.initStorage();
return this.storage.keys()
.then(keys => Promise.all(keys
.filter(key => key.startsWith('data-'))
.map(async key => {
const value = await this.storage.get(key);
return {
key: 'someKey',
value,
}
})
));
}
This yields the same result and is much easier to test. I just needed to add the keys method to my existing spy like this:
mockStorage = jasmine.createSpyObj('Storage', ['get', 'set', 'remove', 'create', 'keys']);
and then mock the return value
mockStorage.keys.and.resolveTo(['different', 'storage', 'keys']);
It's rather straightforward from there. Hope this helps someone in the future.