我是Angular的新手,我正在尝试测试以下组件的构建,这取决于包含BehaviorSubject
的RecipesServices
,称为selectedRecipe
:
@Component({
selector: 'app-recipe',
templateUrl: './recipe.page.html',
styleUrls: ['./recipe.page.scss'],
})
export class RecipePage implements OnInit {
selectedRecipe: Recipe;
constructor(
private recipesService: RecipesService
) {
this.recipesService.selectedRecipe.subscribe(newRecipe => this.selectedRecipe = newRecipe);
}
}
服务如下:
@Injectable({
providedIn: 'root'
})
export class RecipesService {
/**
* The recipe selected by the user
*/
readonly selectedRecipe : BehaviorSubject<Recipe> = new BehaviorSubject(null);
constructor(
private httpClient: HttpClient
) {}
...
}
我已经尝试了很多不同的方法来模拟这个服务,并在组件的测试中将它作为提供者添加,但是我开始缺乏想法。下面是我正在尝试的当前测试,它抛出"Failed: this. recipesservice . selecterecipe .subscribe不是一个函数">:
import { HttpClient } from '@angular/common/http';
import { ComponentFixture, fakeAsync, TestBed, waitForAsync } from '@angular/core/testing';
import { Router, UrlSerializer } from '@angular/router';
import { IonicModule } from '@ionic/angular';
import { BehaviorSubject, defer, Observable, of, Subject } from 'rxjs';
import { Recipe } from '../recipes-list/recipe';
import { RecipesService } from '../recipes-list/services/recipes.service';
import { RecipePage } from './recipe.page';
let mockrecipesService = {
selectedRecipe: BehaviorSubject
}
describe('RecipePage', () => {
let component: RecipePage;
let fixture: ComponentFixture<RecipePage>;
var httpClientStub: HttpClient;
let urlSerializerStub = {};
let routerStub = {};
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ RecipePage ],
imports: [IonicModule.forRoot()],
providers: [
{ provide: HttpClient, useValue: httpClientStub },
{ provide: UrlSerializer, useValue: urlSerializerStub },
{ provide: Router, useValue: routerStub },
{ provide: RecipesService, useValue: mockrecipesService}
]
}).compileComponents();
spyOn(mockrecipesService, 'selectedRecipe').and.returnValue(new BehaviorSubject<Recipe>(null));
fixture = TestBed.createComponent(RecipePage);
component = fixture.componentInstance;
fixture.detectChanges();
}));
it('should create', () => {
expect(component).toBeTruthy();
});
});
谢谢你的帮助!
好问题,有很多代码要看!
首先,我不允许公众从RecipesService访问您的主题,当某些组件开始使用.next方法时,它可能导致失去控制。所以我创建了一个公共可观察对象,并在RecipePage组件中订阅。
另一种味道是构造函数,尽量避免在构造函数中使用逻辑,而使用angular生命周期钩子,比如ngOnInit/ngOnChanges。Angular一旦完成组件的设置,就会调用这个钩子。
对于测试,您只需要模拟RecipesService。如果你的组件不依赖HttpClient,那么你就不需要存根了。
我所做的是创建一个特殊的模拟类来在测试中处理这个服务。通常情况下,您将在不同的组件中使用相同的服务,因此拥有一个可重用的模拟类非常有帮助。RecipesServiceMock有一个公共可观察对象,就像你的真实服务一样($selectedRecipeObs),还有一个辅助方法来在我们的测试中设置新值。
我还创建了一个stackblitz来显示正在运行的测试。你可以在app/pagerecipe/文件夹中找到与你的问题相关的所有内容。如果你想了解更多关于如何测试的想法,可以查看angular的测试教程或不同类型的测试示例以及相应的git repo。
RecipesService:
@Injectable({
providedIn: 'root'
})
export class RecipesService {
/**
* The recipe selected by the user
*/
private readonly selectedRecipe : BehaviorSubject<Recipe> = new BehaviorSubject(null);
// it's good practice to disallow public access to your subjects.
// so that's why we create this public observable to which components can subscibe.
public $selectedRecipeObs = this.selectedRecipe.asObservable();
constructor(
private httpClient: HttpClient
) {}
}
组件:
@Component({
selector: "app-recipe-page",
templateUrl: "./recipe-page.component.html",
styleUrls: ["./recipe-page.component.css"],
providers: []
})
export class RecipePageComponent implements OnInit {
selectedRecipe: Recipe;
constructor(private recipesService: RecipesService) {
// the contructor should be as simple as possible, most code usually goes into one of the life cycle hooks like ngOnInit
}
ngOnInit(): void {
// since we want to avoid any loss of control, we subscribe to the new $selectedRecipeObs instead of the subject.
// everything else goes through your service, set/get etc
this.recipesService.$selectedRecipeObs.subscribe(
newRecipe => (this.selectedRecipe = newRecipe)
);
}
}
我们对RecipesService的模拟:
export class RecipesServiceMock {
private selectedRecipe = new BehaviorSubject<Recipe>(null);
// must have the same name as in your original service.
public $selectedRecipeObs = this.selectedRecipe.asObservable();
constructor() {
}
/** just a method to set values for tests. it can have any name. */
public setSelectedRecipeForTest(value: Recipe): void {
this.selectedRecipe.next(value);
}
}
测试文件:
import {
ComponentFixture,
fakeAsync,
TestBed,
tick,
waitForAsync
} from "@angular/core/testing";
import { Recipe } from "../recipe";
import { RecipesService } from "../recipes.service";
import { RecipesServiceMock } from "../test-recipes.service";
import { RecipePageComponent } from "./recipe-page.component";
////// Tests //////
describe("RecipePageComponent", () => {
let component: RecipePageComponent;
let fixture: ComponentFixture<RecipePageComponent>;
let recipesServiceMock: RecipesServiceMock;
beforeEach(
waitForAsync(() => {
recipesServiceMock = new RecipesServiceMock();
TestBed.configureTestingModule({
imports: [],
providers: [{ provide: RecipesService, useValue: recipesServiceMock }]
}).compileComponents();
fixture = TestBed.createComponent(RecipePageComponent);
component = fixture.componentInstance;
})
);
it("should create", () => {
fixture.detectChanges();
expect(component).toBeTruthy();
});
it("should update component with new value", fakeAsync(() => {
// set new value other than null;
const myNewRecipe = new Recipe("tasty");
recipesServiceMock.setSelectedRecipeForTest(myNewRecipe);
fixture.detectChanges(); //
tick(); // )
expect(component.selectedRecipe).toEqual(myNewRecipe);
}));
});