3

I have a service that acts as a data-store. In it's constructor, it attempts to "hydrate" the data-set from the device's storage (using Ionic and it's Storage service):

@Injectable()
export class SimpleDataStore {

     private _data: BehaviorSubject<any> = new BehaviorSubject<any>(undefined);
     public data: Observable<any> = this._data.asObservable();

     constructor(private _http: HttpClient, private _storage) {
         this.initializeData();
     }

     private initializeData(): void {
          this._storage.get("dataKey")
              .then(data => this._data.next(data))
              .catch(() => this._data.next([]);
     }

}

I know how to write async tests with Jasmine, and how to access private members/methods, as well as knowing to need to check _data.getValue() for my desired result -- but my issue is not knowing how to test:

  1. The constructor, and/or;
  2. initializeData so that it waits for the Promise to finish, since no Promise is being returned in the method.

Thanks for any and all help!

EHorodyski
  • 774
  • 1
  • 8
  • 30
  • If you mock the `_storage` implementation you can control when `this._storage.get("dataKey")` resolves, have you tried doing this? – Jake Holzinger Jan 16 '19 at 23:38
  • I have -- like so `spyOn(TestBed.get(Storage), 'get').and.returnValue(Promise.resolve(localStorage));`. But without setting the test method as `async` -- which I can't really because there's no Promises being returned, I'm kinda lost. – EHorodyski Jan 17 '19 at 12:52

1 Answers1

1

Ideally you would have initializeData() return the promise that it constructs so you can easily wait for it to resolve in your tests. Then instead of trying to figure out when the promise resolves you can simply mock initializeData() before the class is constructed.

Given that the initializeData() method is changed to return the promise you can test the SimpleDataStore class as follows:

describe('SimpleDataStore', function () {

    beforeEach(function () {
        this.initializeData = spyOn(SimpleDataStore.prototype, 'initializeData');
        this.http = jasmine.createSpyObj('HttpClient', ['get']);
        this.storage = jasmine.createSpyObj('Storage', ['get']);
        this.dataStore = new SimpleDataStore(this.http, this.storage);

        // initializeData is called immediately upon construction.
        expect(this.initializeData).toHaveBeenCalled();
    });

    describe('initializeData', function () {

        beforeEach(function () {
            // The data store is initialized, we can now let the calls go to the real implementation.
            this.initializeData.and.callThrough();
        });

        afterEach(function () {
            expect(this.storage.get).toHaveBeenCalledWith('dataKey');
        });

        it('is initialized with "dataKey" storage', function (done) {
            const data = {};
            this.storage.get.and.returnValue(Promise.resolve(data));
            this.dataStore.initializeData()
                .then(() => expect(this.dataStore._data.getValue()).toBe(data))
                .catch(fail)
                .finally(done)
        });

        it('is initialized with empty array when "dataKey" storage rejects', function (done) {
            this.storage.get.and.returnValue(Promise.reject());
            this.dataStore.initializeData()
                .then(() => expect(this.dataStore._data.getValue()).toEqual([]))
                .catch(fail)
                .finally(done)
        });

    });

});
Jake Holzinger
  • 5,783
  • 2
  • 19
  • 33
  • Thanks so much -- I had thought this would be the option available, but wasn't sure if there was any kind of side-effect I'd be missing by having the method return a Promise. – EHorodyski Jan 17 '19 at 13:30
  • The solution isn't perfect, but it works. If you really wanted to avoid side-effects you could introduce the factory pattern, the factory could do the asynchronous work of checking the "storage", then construct the object synchronously with the result. – Jake Holzinger Jan 17 '19 at 22:34