AngularFireDatabase、Jest 和单元测试 firebase 实时数据库
AngularFireDatabase, Jest and unit testing firebase realtime database
我有一个服务有 2 个方法,return 来自 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);
});
}));
});
allProductsMock
和 singleProductMock
只是本地文件中的虚拟数据。
抛出的错误是this.db.list
is not a function.
如果我将存根更改为基本常量而不是 class,则 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');
});
}));
});
使用 class 方法,您的做法有点不对。不过,您可以同时使用 class 或常量。此外,您不应该在单元测试中导入 AngularFireModule
,并且绝对不初始化它。这会大大减慢您的测试速度,因为我可以想象它需要加载整个 firebase
模块,仅用于您实际模拟 firebase 的单元测试。
所以你需要模拟的是AngularFireDatabase
。这个class有list
、object
、createPushId
三个方法。我怀疑对于这个测试用例,您只会使用前两个。因此,让我们创建一个执行此操作的对象:
// 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();
});
}));
});
通过使用 list
和 key
变量,您可以使用不同类型的值进行多个测试来测试边缘情况。看看它是否仍然 return 符合您的预期 return
我有一个服务有 2 个方法,return 来自 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);
});
}));
});
allProductsMock
和 singleProductMock
只是本地文件中的虚拟数据。
抛出的错误是this.db.list
is not a function.
如果我将存根更改为基本常量而不是 class,则 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');
});
}));
});
使用 class 方法,您的做法有点不对。不过,您可以同时使用 class 或常量。此外,您不应该在单元测试中导入 AngularFireModule
,并且绝对不初始化它。这会大大减慢您的测试速度,因为我可以想象它需要加载整个 firebase
模块,仅用于您实际模拟 firebase 的单元测试。
所以你需要模拟的是AngularFireDatabase
。这个class有list
、object
、createPushId
三个方法。我怀疑对于这个测试用例,您只会使用前两个。因此,让我们创建一个执行此操作的对象:
// 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();
});
}));
});
通过使用 list
和 key
变量,您可以使用不同类型的值进行多个测试来测试边缘情况。看看它是否仍然 return 符合您的预期 return