3

I have a test file that is testing a service that returns data from AngularFireDatabase:

import {TestBed, async} from '@angular/core/testing';
import {ProductService} from './product.service';
import {AngularFireDatabase} from '@angular/fire/database';
import {productsMock} from '../../../../mocks/products.mock';
import {Product} from 'shared';
import {Observable} from 'rxjs';
import {getSnapShotChanges} from 'src/app/test/helpers/AngularFireDatabase/getSnapshotChanges';

let list: Product[];
let key: string = '';

const afDatabaseStub = {
  db: jest.fn().mockReturnThis(),
  list: jest.fn(() => ({
    snapshotChanges: jest.fn().mockReturnValue(getSnapShotChanges(list, true)),
    valueChanges: jest.fn(
      () => new Observable(sub => sub.next(Object.values(list)))
    )
  })),
  object: jest.fn(() => ({
    valueChanges: jest.fn(() => new Observable(sub => sub.next({id: key})))
  }))
};

describe('ProductService', () => {
  let service: ProductService;

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [{provide: AngularFireDatabase, useValue: afDatabaseStub}]
    });
    service = TestBed.inject(ProductService);
  });

  it('should be created', () => {
    expect(service).toBeTruthy();
  });

  describe('getAllProducts', () => {
    it('should be able to return all products', async(() => {
      list = productsMock;

      service.getAllProducts().subscribe((products: Product[]) => {
        expect(products?.length).toEqual(10);
      });
    }));
  });

  it('should be able to return a single product using the firebase id', async(() => {
    key = '-MA_EHxxDCT4DIE4y3tW'
    const response$ = service.getProductById(key);
    response$.subscribe((giveawayProduct: GiveawayProduct) => {
      expect(giveawayProduct).toBeDefined();
      expect(giveawayProduct.id).toEqual(key);
    });
  }));

});

The problem I am facing is that I now want to test another service that also uses AngularFireDatabase.

So how can I make this stub more general purpose and put it into a shared helper file that I can use in different specs?

For example, I know you can do useClass instead of useValue:

providers: [{provide: AngularFireDatabase, useClass: afDatabaseStub}]

If it was a class then list and key could be class properties that I could set before running the tests.

But when I try that, I get errors like this:

db.list.object is not a function

db.list(...).snapshotchanges is not a function

2 Answers 2

2
+200

I think the best way to do this is to create an abstraction layer between AngularFire and your components. Something like this:

interface IProductService {
    getProducts(): Observable<product>;
    getProduct(id: string): Observable<product>;
    //And all your other methods.
}

Now create your product service which implements the interface:

ProductService implements IProductService {
    constructor(angularFire: AngularFire){}

    getProducts(): Observable<product>{
        return this.angularFire....
    }
    //And all your other methods.
}

Now for your tests you can create a veery simple mock instance:

MockProductService implements IProductService {
    constructor(){}

    getProducts(): Observable<product>{
        return of([new Product("One"), new Product("Two")])
    }
    //And all your other methods.
}

You can make your mock as easy or as complex as you need it.

Sign up to request clarification or add additional context in comments.

2 Comments

Thanks, so by using an interface we completely avoid the need to create an angularFire stub, correct? We just implement a mock version of the service that returns data directly? That seems too easy if I understand this correctly!
It is that easy
1

All you need you already have. Next thing you need to do is to extract mock object into some .ts file and export it.

I usually create test-helpers folder and providers.ts in it. There I declare some functions that provide me most common providers mocks.

export function getAngularFireMock() {
  const afDatabaseStub = {
    db: jest.fn().mockReturnThis(),
    list: jest.fn(() => ({
      snapshotChanges: jest.fn().mockReturnValue(getSnapShotChanges(list, true)),
      valueChanges: jest.fn(
        () => new Observable(sub => sub.next(Object.values(list)))
      )
    })),
    object: jest.fn(() => ({
      valueChanges: jest.fn(() => new Observable(sub => sub.next({id: key})))
    }))
  };

  return {provide: AngularFireDatabase, useValue: afDatabaseStub};
}

Then you simply call getAngularFireMock() inside providers array and that's it.

Then if you need change values for other tests you simply mock them using Jest's API.

4 Comments

thanks but that isn't going to work. If list is mocked product data for one set of tests but customers for another set, how are you going to change what list contains? In your helper, 'list' is a locally defined variable and thus will always contain whatever data you declare there. It cannot change from one test to another.
You are creating a mock. Then you need to inject your service using TestBed capabilities. After that using Jest API you can rewrite spies return values. I can show you an example using Jasmine. It's not far from Jest but shows the use case. The idea of Spies is that they are dynamic and you can change them at any time.
Sorry but I think you have misunderstood. All your answer does is take my code and put it into an external function that can be imported. Object.values(list) is not going to return any data because list is not defined in your code. In my code list is of type Product[] and the data is coming from the imported productsMock file. So what happens when list needs to be of type Customer[]? How is your code going to provide mocked customer data to list so that the stub can return it? THAT is the entire point of my question.
As you wish. Read the docs on how you can change returned value by mock. All you need is to define mock for any method/property you ever need and then in places you can specify what you expect from them to return.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.