AngularFireDatabase、Jest和单元测试Firebase实时数据库

6

我有一个服务,其中包含两个方法,可以从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 不是一个函数。

如果我将存根更改为基本常量而不是类,则所有产品测试将通过,但显然我将无法测试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');
    });
  }));
});

1个回答

4

采用类的方法,您的做法有些不对。不过,您既可以使用类也可以使用常量。同时,在单元测试中,您不应导入 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不是一个函数,因为您将其放置在对list的调用内部。正如您从我的代码中看到的,我必须使listsnapshotChanges成为2个单独的jest函数。那么在list中,snapshotChanges也不应该是jest.fn函数吗? - rmcsharry
@rmcsharry 对,那是我的笔误 :) 我扩展了我在项目中使用的内容,但忘记了进行适当的测试。我已更新答案,使 snapshotChanges 也返回 jest.fn - Poul Kruijt

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接