Angular 单元测试 - 如何从注入的服务触发另一个发射?

Angular Unit Testing - How to trigger another emit from an injected service?

我有一个组件执行以下操作:

  1. 在构造函数中,它获取查询参数并将它们解析为给定的季度和年份

  2. 在 ngOnInit 中,它订阅了一个 returns 季度和年份列表的服务。在订阅的回调中,它使用季度和年份的值将值分配给查询参数中选择的季度和年份,如果它们无效,则分配给列表中的第一个。

在我的单元测试中,我能够设置参数(或至少设置快照),但我想在订阅方法内重新触发回调并断言一些期望。我不确定如何在订阅方法中重新触发回调。

下面是我的组件的相关代码:

@Component({
    ...
})
export class MyComponent implements OnInit {
quarter: number;
year: number;
paramQuarter: string;
selectedQuarter: IQuarter;
quartersList: IQuarter[];

constructor(
            private quarterService: QuarterService,
            private activatedRoute: ActivatedRoute,
            private router : Router) {

    // if no query param is set, set as the current quarter and year from moment.js
    this.paramQuarter = this.activatedRoute.snapshot.queryParams['quarter'] 
        || moment().quarter() + '-' + moment().year();

}

ngOnInit(): void {

    this.quarterService.getQuarters().subscribe(response => {
        this.quartersList = response;
        /// ----------------------------------
        /// The code I want to test is in here
        /// ----------------------------------
        const [quarter, year] = this.paramQuarter.split("-").map(string => parseInt(string));
        // get the correct quarter based on URL params, or the current one if the url param is malformed
        this.selectedQuarter: IQuarter = this.quartersList
            .filter(q => q.quarter === quarter && q.year === year)[0]
            || this.quartersList[0];
    });
}

}

和测试:

const MOCK_QUARTERS = [
{
    quarter: 2,
    year: 2020,
    startDate: '2020-04-01',
    endDate: '2020-06-30',
},
{
    quarter: 1,
    year: 2020,
    startDate: '2020-01-01',
    endDate: '2020-03-31',
}
]

describe('MyComponent', () => {
let component: MyComponent;
let fixture: ComponentFixture<MyComponent>;
let quarterService: QuarterService;
let router : Router;
let activatedRoute : ActivatedRoute;

const MockQuarterService = {
    getQuarters: jasmine.createSpy('getQuarters').and.returnValue(
    of(MOCK_QUARTERS)
    )
}

beforeEach(async(() => {
    TestBed.configureTestingModule({
    declarations: [
        MyComponent,
    ],
    imports: [
        SharedModule,
        RouterTestingModule,
        ...HttpClientTestingImports,
    ],
    providers: [
        ...HttpClientTestingProviders,
        { provide: QuarterService, useValue: MockQuarterService },
        {
        provide: ActivatedRoute,
        useValue: {
            queryParams: of({ quarter: '2-2020' }),
            snapshot: {
            queryParams: { quarter: '2-2020' }
            }
        }
        },
    ]
    })
    .compileComponents();
}));

beforeEach(() => {
    fixture = TestBed.createComponent(MyComponent);
    component = fixture.componentInstance;
    quarterService = TestBed.inject(QuarterService);
    router = TestBed.inject(Router);
    activatedRoute = TestBed.inject(ActivatedRoute);
    fixture.detectChanges();
});

fit('should set the current quarter from correctly formed URL params', async(() => {
    // set from the params in the beforeEach callback
    expect(component.selectedQuarter).toEqual(MOCK_QUARTERS[0]);
    spyOn(component, 'updateQuarterSelection').and.callThrough();

    let actRoute : ActivatedRoute = fixture.debugElement.injector.get(ActivatedRoute);
    actRoute.queryParams = of( {quarter: `${MOCK_QUARTERS[1].quarter}-${MOCK_QUARTERS[1].year}` } );
    actRoute.snapshot.queryParams.quarter = `${MOCK_QUARTERS[1].quarter}-${MOCK_QUARTERS[1].year}`;
    /// ----------------------------------
    // Somehow need the callback of the quarterService.getQuarters().subscribe() 
    // method to retrigger here
    /// ----------------------------------
    fixture.detectChanges();
    console.log(component);
    
    expect(quarterService.getQuarters).toHaveBeenCalledTimes(2);
    expect(component.updateQuarterSelection).toHaveBeenCalledTimes(2);
    expect(component.selectedQuarter).toEqual(MOCK_QUARTERS[1]);
}));

});

要快速解锁,您只需再次调用 ngOnInit

fit('should set the current quarter from correctly formed URL params', async(() => {
    // set from the params in the beforeEach callback
    expect(component.selectedQuarter).toEqual(MOCK_QUARTERS[0]);
    spyOn(component, 'updateQuarterSelection').and.callThrough();

    let actRoute : ActivatedRoute = fixture.debugElement.injector.get(ActivatedRoute);
    actRoute.queryParams = of( {quarter: `${MOCK_QUARTERS[1].quarter}-${MOCK_QUARTERS[1].year}` } );
    actRoute.snapshot.queryParams.quarter = `${MOCK_QUARTERS[1].quarter}-${MOCK_QUARTERS[1].year}`;
    
    component.ngOnInit();    

    fixture.detectChanges();
    console.log(component);
    
    expect(quarterService.getQuarters).toHaveBeenCalledTimes(2);
    expect(component.updateQuarterSelection).toHaveBeenCalledTimes(2);
    expect(component.selectedQuarter).toEqual(MOCK_QUARTERS[1]);
}));

要获得更详细的答案,您调用的第一个 fixture.detectChanges() 是在调用 ngOnInit 时。因此,当调用构造函数 (createComponent) 时,您可以对 beforeEach 进行策略性的处理(我不会展示此方法,因为它需要更多的簿记)。我们可以利用 BehaviorSubject 重新触发一些订阅。

import { BehaviorSubject } from 'rxjs';
....
// replace any with Quarter interface
const mockQuarters = new BehaviorSubject<any>(MOCK_QUARTERS);
const MockQuarterService = {
    getQuarters: () => mockQuarters, // return BehaviorSubject
}

...
beforeEach(async(() => {
    TestBed.configureTestingModule({
    declarations: [
        MyComponent,
    ],
    imports: [
        SharedModule,
        RouterTestingModule,
        ...HttpClientTestingImports,
    ],
    providers: [
        ...HttpClientTestingProviders,
        { provide: QuarterService, useValue: MockQuarterService },
        {
        provide: ActivatedRoute,
        useValue: {
            queryParams: of({ quarter: '2-2020' }),
            snapshot: {
            queryParams: { quarter: '2-2020' }
            }
        }
        },
    ]
    })
    .compileComponents();
}));

beforeEach(() => {
    fixture = TestBed.createComponent(MyComponent);
    component = fixture.componentInstance;
    quarterService = TestBed.inject(QuarterService);
    router = TestBed.inject(Router);
    activatedRoute = TestBed.inject(ActivatedRoute);
    fixture.detectChanges();
});

fit('should set the current quarter from correctly formed URL params', async(() => {
    // set from the params in the beforeEach callback
    expect(component.selectedQuarter).toEqual(MOCK_QUARTERS[0]);
    spyOn(component, 'updateQuarterSelection').and.callThrough();

    let actRoute : ActivatedRoute = fixture.debugElement.injector.get(ActivatedRoute);
    actRoute.queryParams = of( {quarter: `${MOCK_QUARTERS[1].quarter}-${MOCK_QUARTERS[1].year}` } );
    actRoute.snapshot.queryParams.quarter = `${MOCK_QUARTERS[1].quarter}-${MOCK_QUARTERS[1].year}`;

    // this should retrigger the subscription
    mockQuarters.next(MOCK_QUARTERS);
    
    fixture.detectChanges();
    console.log(component);
    
    expect(quarterService.getQuarters).toHaveBeenCalledTimes(2);
    expect(component.updateQuarterSelection).toHaveBeenCalledTimes(2);
    expect(component.selectedQuarter).toEqual(MOCK_QUARTERS[1]);
}));