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

rmc*_*rry 6 unit-testing firebase angularfire jestjs angular

我有一个服务,它有 2 种方法可以从 firebase 实时数据库返回数据

getAllProducts -> returns an observable array of products
getSingleProduct -> returns an observable single product
Run Code Online (Sandbox Code Playgroud)

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

测试文件

getAllProducts -> returns an observable array of products
getSingleProduct -> returns an observable single product
Run Code Online (Sandbox Code Playgroud)

allProductsMock并且singleProductMock只是本地文件中的虚拟数据。

抛出的错误this.db.list不是函数。

如果我将存根更改为基本常量而不是类,则 allProducts 测试通过,但显然我随后无法测试该getSingleProduct方法:

    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);
        });
      }));
    });
Run Code Online (Sandbox Code Playgroud)

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

帮手

getSnapshotChanges 是帮手:

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

更新

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

    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;
      }
    }
Run Code Online (Sandbox Code Playgroud)

Pie*_*Duc 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]
    )))    
  }))
}
Run Code Online (Sandbox Code Playgroud)

现在是初始化和实施测试的时候了:

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();
    });
  }));
});

Run Code Online (Sandbox Code Playgroud)

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