angularunit-testingionic-frameworkangular2-testingionic-storage

Ionic Storage v3 - Unit Tests | TypeError: _this.storage.forEach is not a function


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?


Solution

  • 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.