How do I get tick
to work, or at least, how do I get the test to advance 10s to appropriately call submit
in my component?
Note: I don't want to do await new Promise(r => setTimeout(r, 10000))
because it will make my tests run long, and tests are supposed to be short
I want submit
to only call cb
after 10 seconds has elapsed from the creation of the component
I have a timer in my component that completes after 10s. This timer changes a subject from false to true, and is used to determine whether or not it is considered valid to submit the data in the component.
In testing, tick
does not seem to advance the timer at all, and it actually runs a full 10s. I attempted to fix this by putting a fakeAsync
in the beforeEach
that creates the component to no avail.
fakeAsync
in the test component init, as well as the testfakeAsync
in only the testsetTimeout(() => this.obs.next(true), 10_000)
instead of timerempty().pipe(delay(10000)).subscribe(() => this.obs.next(true));
instead of timertimer
in ngOnInit
instead of constructortimer
in constructor instead of ngOnInit
If you adjust this code
timer(10_000).subscribe(() => this.testThis$.next(true));
to instead be this
timer(10_000).subscribe(() => {
debugger;
this.testThis$.next(true)
});
You will find that every time a test runs, the Javascript debugger in Dev Tools is triggered 10 seconds after component creation (instead of instantly if tick worked).
Here's the code. At the bottom is a link to a minimal reproduction on GitHub.
// component code
import { Component, OnInit, Inject } from '@angular/core';
import { BehaviorSubject, Subject, timer } from 'rxjs';
import { first, filter } from 'rxjs/operators';
@Component({
selector: 'app-tick-test',
templateUrl: './tick-test.component.html',
styleUrls: ['./tick-test.component.scss']
})
export class TickTestComponent implements OnInit {
public testThis$: Subject<boolean>;
constructor(
@Inject('TICK_CALLBACK') private readonly cb: () => void,
) {
this.testThis$ = new BehaviorSubject<boolean>(false);
timer(10_000).subscribe(() => this.testThis$.next(true));
}
public ngOnInit(): void {
}
public submit(): void {
// call the callback after 10s
this.testThis$
.pipe(first(), filter(a => !!a))
.subscribe(() => this.cb());
}
}
// test code
/**
* The problem in this one is that I am expecting `tick` to advance the
* time for the timer that was created in the constructor, but it is not working
*/
import { async, ComponentFixture, TestBed, tick, fakeAsync } from '@angular/core/testing';
import { TickTestComponent } from './tick-test.component';
describe('TickTestComponent', () => {
let component: TickTestComponent;
let fixture: ComponentFixture<TickTestComponent>;
let callback: jasmine.Spy;
beforeEach(async(() => {
callback = jasmine.createSpy('TICK_CALLBACK');
TestBed.configureTestingModule({
providers: [
{ provide: 'TICK_CALLBACK', useValue: callback },
],
declarations: [ TickTestComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(TickTestComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should be true after 10s', fakeAsync(() => {
tick(10_001);
component.submit();
expect(callback).toHaveBeenCalled();
}));
});
fixture.detectChanges()
into each test, and call tick(10_000)
there.timer(10_000)...
to the ngOnInit
in the componentWhenever you use fakeAsync
a "zone" that your code can run in is made. From my observations, this zone "exists" until it goes out of scope. By using fakeAsync
in the beforeEach
, you destroy the zone and you get issues with the timer not finishing (despite the timer not finishing being the desireable outcome).
You want to move the timer
into the ngOnInit
because it is NOT called immediately upon the .createComponent
call. It is instead called when you run fixture.detectChanges()
for the first time. Thus when you call fixture.detectChanges()
inside of a test's fakeAsync
zone for the first time, ngOnInit
is called for you, the timer is captured in the zone, and you can control time as expected.
describe('TickTestComponent', () => {
let component: TickTestComponent;
let fixture: ComponentFixture<TickTestComponent>;
let callback: jasmine.Spy;
beforeEach(async(() => {
callback = jasmine.createSpy('TICK_CALLBACK');
TestBed.configureTestingModule({
providers: [
{ provide: 'TICK_CALLBACK', useValue: callback },
],
declarations: [ TickTestComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(TickTestComponent);
component = fixture.componentInstance;
// don't run this here
// fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should be true after 10s', fakeAsync(() => {
// this calls ngOnInit if it is the first detectChanges call
fixture.detectChanges();
tick(10_001);
component.submit();
expect(callback).toHaveBeenCalled();
}));
});