【问题标题】:AngularFireDatabase, Jest and unit testing firebase realtime databaseAngularFireDatabase、Jest 和单元测试 firebase 实时数据库
【发布时间】:2020-10-17 05:52:43
【问题描述】:

我有一个服务有 2 个从 firebase 实时数据库返回数据的方法

getAllProducts -> returns an observable array of products
getSingleProduct -> returns an observable single product

我正在尝试使用 Jest 创建单元测试来模拟 firebase,以便我可以测试这两种方法:

测试文件

    import {TestBed, async} from '@angular/core/testing';
    import {ProductService} from './product.service';
    import {AngularFireModule} from '@angular/fire';
    import {environment} from 'src/environments/environment';
    import {AngularFireDatabase} from '@angular/fire/database';
    import {getSnapShotChanges} from 'src/app/test/helpers/AngularFireDatabase/getSnapshotChanges';
    import {Product} from './product';
    
    class angularFireDatabaseStub {
      getAllProducts = () => {
        return {
          db: jest.fn().mockReturnThis(),
          list: jest.fn().mockReturnThis(),
          snapshotChanges: jest
            .fn()
            .mockReturnValue(getSnapShotChanges(allProductsMock, true))
        };
      };
      getSingleProduct = () => {
        return {
          db: jest.fn().mockReturnThis(),
          object: jest.fn().mockReturnThis(),
          valueChanges: jest.fn().mockReturnValue(of(productsMock[0]))
        };
      };
    }
    
    describe('ProductService', () => {
      let service: ProductService;
    
      beforeEach(() => {
        TestBed.configureTestingModule({
          imports: [AngularFireModule.initializeApp(environment.firebase)],
          providers: [
            {provide: AngularFireDatabase, useClass: angularFireDatabaseStub}
          ]
        });
        service = TestBed.inject(ProductService);
      });
    
      it('should be created', () => {
        expect(service).toBeTruthy();
      });
    
      it('should be able to return all products', async(() => {
        const response$ = service.getAllProducts();
        response$.subscribe((products: Product[]) => {
          expect(products).toBeDefined();
          expect(products.length).toEqual(10);
        });
      }));
    });

allProductsMocksingleProductMock 只是本地文件中的虚拟数据。

抛出的错误是this.db.list is not a function。

如果我将存根更改为基本常量而不是类,则 allProducts 测试通过,但显然我在测试 getSingleProduct 方法时遇到了困难:

    const angularFireDatabaseStub = {
      db: jest.fn().mockReturnThis(),
      list: jest.fn().mockReturnThis(),
      snapshotChanges: jest
        .fn()
        .mockReturnValue(getSnapShotChanges(allProductsMock, true))
      };
    }

那么我怎样才能使存根更加通用,并且还能够测试getSingleProduct 方法呢?

助手

getSnapshotChanges 是助手:

    import {of} from 'rxjs';
    
    export function getSnapShotChanges(data: object, asObservable: boolean) {
      const actions = [];
      const dataKeys = Object.keys(data);
      for (const key of dataKeys) {
        actions.push({
          payload: {
            val() {
              return data[key];
            },
            key
          },
          prevKey: null,
          type: 'value'
        });
      }
      if (asObservable) {
        return of(actions);
      } else {
        return actions;
      }
    }

更新

我确实找到了一种方法来进行这两项测试,但是必须设置两次 TestBed 并不是很干燥。肯定有一种方法可以将两个存根组合起来并只将它们注入 TestBed 一次吗?

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

const getAllProductsStub = {
  db: jest.fn().mockReturnThis(),
  list: jest.fn().mockReturnThis(),
  snapshotChanges: jest
    .fn()
    .mockReturnValue(getSnapShotChanges(productsMock, true))
};
const getSingleProductStub = {
  db: jest.fn().mockReturnThis(),
  object: jest.fn().mockReturnThis(),
  valueChanges: jest.fn().mockReturnValue(of(productsMock[0]))
};

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

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [AngularFireModule.initializeApp(environment.firebase)],
      providers: [{provide: AngularFireDatabase, useValue: getAllProductsStub}]
    }).compileComponents();
    service = TestBed.inject(ProductService);
  });

  it('should be able to return all products', async(() => {
    const response$ = service.getAllProducts();
    response$.subscribe((products: Product[]) => {
      expect(products).toBeDefined();
      expect(products.length).toEqual(10);
    });
  }));
});

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

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [AngularFireModule.initializeApp(environment.firebase)],
      providers: [{provide: AngularFireDatabase, useValue: getSingleProductStub}]
    }).compileComponents();
    service = TestBed.inject(ProductService);
  });

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

【问题讨论】:

    标签: angular firebase unit-testing jestjs angularfire


    【解决方案1】:

    使用类方法,您的做法有点错误。不过,您可以同时使用类或常量。此外,您不应该在单元测试中导入AngularFireModule,并且绝对不要初始化它。这会大大降低您的测试速度,因为我可以想象它需要加载整个 firebase 模块,仅用于您实际模拟 firebase 的单元测试。

    所以你需要模拟的是AngularFireDatabase。这个类有三个方法,listobjectcreatePushId。我怀疑对于这个测试用例,您只会使用前两个。因此,让我们创建一个执行此操作的对象:

    // your list here
    let list: Record<string, Product> = {};
    // your object key here
    let key: string = '';
    
    // some helper method for cleaner code
    function recordsToSnapshotList(records: Record<string, Product>) {
      return Object.keys(records).map(($key) => ({
        exists: true,
        val: () => records[$key],
        key: $key
      }))
    }
    
    // and your actual mocking database, with which you can override the return values
    // in your individual tests
    const mockDb = {
      list: jest.fn(() => ({
        snapshotChanges: jest.fn(() => new Observable((sub) => sub.next(
          recordsToSnapshotList(list)
        ))),
        valueChanges: jest.fn(() => new Observable((sub) => sub.next(
          Object.values(list)
        )))
      })),
      object: jest.fn(() => ({
        snapshotChanges: jest.fn(() => new Observable((sub) => sub.next(
          recordsToSnapshotList({ [key]: {} as Product })[0]
        ))),
        valueChanges: jest.fn(() => new Observable((sub) => sub.next(
          Object.values({ [key]: {} })[0]
        )))    
      }))
    }
    

    现在是初始化和执行测试的时候了:

    describe('ProductService', () => {
      let service: ProductService;
    
      // using the mockDb as a replacement for the database. I assume this db is injected
      // in your `ProductService`
      beforeEach(() => {
        TestBed.configureTestingModule({
          providers: [{ provide: AngularFireDatabase, useValue: mockDb }]
        });
    
        service = TestBed.inject(ProductService);
      });
    
      it('should be able to return all products', async((done) => {
        // setting the return value of the observable
        list = productsMock;
    
        service.getAllProducts().subscribe((products: Product[]) => {
          expect(products?.length).toEqual(10);
          done();
        });
      }));
    
      it('should be able to return a single product using the firebase id', async((done) => {
        key = '-MA_EHxxDCT4DIE4y3tW';
    
        service.getSingleProduct(key).subscribe((product: Product) => {
          expect(product?.id).toEqual(key);
          done();
        });
      }));
    });
    
    

    通过使用listkey 变量,您可以使用不同类型的值进行多个测试来测试边缘情况。看看它是否仍然返回您期望它返回的内容

    【讨论】:

    • 感谢您的详尽回答,但它会引发错误 this.db.list(...).snapshotChanges is not a function -> 如您所见,snapshotChanges 不是您的 mockDb 中的函数,因为您已将其放在对 @ 的调用中987654333@。正如您从我的代码中看到的那样,我必须将 list 和 snapshotChanges 作为 2 个独立的 jest 函数。那么snapshotChanges 不应该也是jest.fn 内的list 吗?
    • @rmcshary 对,这是我的错字:) 我扩展了我在项目中使用的内容,但忘记正确测试它。我已经更新了答案以使 snapshotChanges 也返回 jest.fn
    猜你喜欢
    • 2019-04-13
    • 2015-04-10
    • 2021-10-13
    • 1970-01-01
    • 2019-01-02
    • 2018-12-16
    • 2020-08-04
    • 1970-01-01
    相关资源
    最近更新 更多