MartaGalve
MartaGalve

Reputation: 1226

Change value of observable returned by mocked service for each test in Angular/Jasmine/Redux

I have a service that returns the values exposed by ngrx selectors. A component defines this service to get data. I am writing unit tests for the component using a mock of the service, and I need the mock service to return different values for each unit test. How can I achieve this?

Component

@Component({
    selector: 'app-test',
    templateUrl: './test.component.html',
    providers: [TestService],

})
    export class TestComponent {

        test$ = this.testService.test$;
        test: number;

        constructor(private service: TestService) {
            service.test$.subscribe(test => this.test = test);
        }
    }

Service

export class TestService {

        test$ = this.store.select(getTestValueFromStore);

        constructor(private store: Store<any>){}
}

Attempt 1 (reset the value of the service): does not work

class MockTestService {
    **test$ = of(10);**
}

describe('TestComponent', () => {
    let testService: TestService;

    beforeEach((() => {
        // Define component service
        TestBed.overrideComponent(
            TestComponent,
            { set: { providers: [{ provide: TestService, useClass: MockTestService }] } }
        );

        TestBed.configureTestingModule({
            declarations: [TestComponent]
        })
            .compileComponents();
    }));

    beforeEach(async() => {
        fixture = TestBed.createComponent(TestComponent);
        component = fixture.componentInstance;
        testService = fixture.debugElement.injector.get(TestService);
        fixture.detectChanges();
    });

it('should do something when the value returned by the service is 20', fakeAsync(() => {

        **testService.test$ = of(20);**

        tick();

        expect(component.test).toEqual(20);
    }));
});

Attempt 2: use subjects. Karma throws error "Property 'next' does not exist on type 'Observable'" because TestService returns observables, not subjects

class MockTestService {
        **test$ = new BehaviourSubject(10);**
    }

    describe('TestComponent', () => {
        let testService: TestService;

        beforeEach((() => {
            // Define component service
            TestBed.overrideComponent(
                TestComponent,
                { set: { providers: [{ provide: TestService, useClass: MockTestService }] } }
            );

            TestBed.configureTestingModule({
                declarations: [TestComponent]
            })
                .compileComponents();
        }));

        beforeEach(async() => {
            fixture = TestBed.createComponent(TestComponent);
            component = fixture.componentInstance;
            testService = fixture.debugElement.injector.get(TestService);
            fixture.detectChanges();
        });

    it('should do something when the value returned by the service is 20', fakeAsync(() => {

            **testService.test$.next(20);**

            tick();

            expect(component.test).toEqual(20);
        }));
    });

Upvotes: 15

Views: 17858

Answers (3)

Brendan
Brendan

Reputation: 1055

If you are trying to test a non-component Angular item (ie AuthGuard with Service dependencies), you won't be able to use the fixture.detectChanges() trick as you won't have a component to detect changes for.

In this case you should use a spy:

describe('AuthGuardTest', () => {
    let testBed: TestBed;
    let authGuard: AuthGuard;
    let testService: TestService;

    beforeEach(() => {
      TestBed.configureTestingModule({
        imports: [HttpClientTestingModule],
        providers: [AuthGuard]
      });

      testBed = getTestBed();
      testService = testBed.inject(TestService);
      authGuard = testBed.inject(AuthGuard);
    });

    it('should return 1000', () => {
      // Mock the method in your testService to return 1000
      spyOn(testService, 'getNumber').and.returnValue(1000);

      // Call code that references testService.getNumber() and Assert 1000 was returned
    });

    it('should return 2000', () => {
      // Mock the method in your testService to return 2000
      spyOn(testService, 'getNumber').and.returnValue(2000);

      // Call code that references testService.getNumber() and Assert 2000 was returned
    });
});

For more examples check out these tutorials:

Mocks and Spies - https://codecraft.tv/courses/angular/unit-testing/mocks-and-spies/

Unit Testing AuthGuard - https://keepgrowing.in/angular/how-to-test-angular-authguard-examples-for-the-canactivate-interface/

Upvotes: 1

dmcgrandle
dmcgrandle

Reputation: 6070

You said in your question that you want "the mock service to return different values for each unit test." To do this you are going to have to make some changes to the way your component is defined.

  1. The most important change is to move the subscription into ngOnInit() and out of the constructor for your component. If you do this, then you can control when the subscription fires, otherwise it will fire when the component is created making it very difficult to return different values for each unit test.

  2. Once that is done, then you need to be careful where you call fixture.detectChanges(). This is because that will execute ngOnInit(), which will execute the subscription and store the value returned from the Observable into component.test. Note that since you are using of() it will simply emit once and then complete. It will not emit again if you put a new value into testService.test$.

  3. Once you have this set up, then you can change the value of testService.test$ BEFORE you call fixture.detectChanges and make the values whatever you would like.

I have set up a StackBlitz to demonstrate this, changing your code as little as possible just to get it to work. There are two tests, each testing different values returned.

I hope this helps!

Upvotes: 24

Tomasz Kula
Tomasz Kula

Reputation: 16837

Try something like this

describe('TestComponent', () => {
    beforeEach((() => {
        TestBed.configureTestingModule({
            declarations: [TestComponent],
            providers: [
              { provide: TestService, useValue: { test$: of(20) } }
            ]
        })
            .compileComponents();
    }));

    beforeEach(async() => {
        fixture = TestBed.createComponent(TestComponent);
        component = fixture.componentInstance;
        testService = fixture.debugElement.injector.get(TestService);
        fixture.detectChanges();
    });

   it('should do something when the value returned by the service is 20', () => {
        expect(component.test).toEqual(20);
    });
});

You do not need to use fakeAsync here, because of() emits synchronously.

Also, please consider subscribing to the observable in the ngOnInit hook. Constructor should be used for dependency injection only.

Upvotes: 1

Related Questions