Search code examples
angularangular-spectator

angular spectator mock Observable<string>


I am trying to write a (fairly) complicated test using https://github.com/ngneat/spectator I have a component which has 3 http services in ngOnInit.

Two of them are easy to mock, I can do it 2 ways (I think):

const FieldServiceMock = {
    list: (categoryId: string, specificationOnly: boolean) =>
        of([
            {
                id: 0,
                canCopy: false,
                categoryId: 'cameras',
                dataType: 0,
                display: false,
                isSpecification: true,
                name: 'gtin',
                order: 0,
                required: true,
            },
        ]),
};

const ProductServiceMock = {
    listValidatedRangeProducts: (categoryId: string) =>
        of([
            {
                gtin: 0,
            },
            {
                gtin: 1,
            },
        ]),
};

let spectator: Spectator<SpecificationsSaveComponent>;
const createComponent = createComponentFactory({
    component: SpecificationsSaveComponent,
    imports: [],
    providers: [
        mockProvider(FieldService, FieldServiceMock),
        mockProvider(ProductService, ProductServiceMock)
    ],
});

or

let spectator: Spectator<SpecificationsSaveComponent>;
const createComponent = createComponentFactory({
    component: SpecificationsSaveComponent
});

beforeEach(() => {
    spectator = createComponent();
    spectator.get(FieldService).list.andReturn(of([
        {
            id: 0,
            canCopy: false,
            categoryId: 'cameras',
            dataType: 0,
            display: false,
            isSpecification: true,
            name: 'gtin',
            order: 0,
            required: true,
        },
    ]));
    spectator.get(ProductService).listValidatedRangeProducts.andReturn(of([
        {
            gtin: 0,
        },
        {
            gtin: 1,
        },
    ]));
});

The problem is, I have another service that doesn't quite work like the other two. The service looks like this:

import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';

import { Category } from '@models';

@Injectable({
    providedIn: 'root',
})
export class SelectorService {
    private currentCategoriesSubject: BehaviorSubject<Category[]>;
    private currentCategorySubject: BehaviorSubject<string>;

    public categories: Observable<Category[]>;
    public category: Observable<string>;

    constructor() {
        this.currentCategoriesSubject = new BehaviorSubject<Category[]>(null);
        this.categories = this.currentCategoriesSubject.asObservable();

        this.currentCategorySubject = new BehaviorSubject<string>(null);
        this.category = this.currentCategorySubject.asObservable();
    }

    public setCategories(categories: Category[]): void {
        if (!categories.length) return;
        this.setCategory(categories[0].id);
        this.currentCategoriesSubject.next(categories);
    }

    public setCategory(categoryId: string): void {
        this.currentCategorySubject.next(categoryId);
    }

    public get currentCategory(): string {
        return this.currentCategorySubject.value;
    }

    public add(category: Category) {
        let categories = this.currentCategoriesSubject.value;
        if (!categories) categories = [];

        categories.push(category);

        this.currentCategoriesSubject.next(categories);
        this.currentCategorySubject.next(category.id);
    }

    public remove(categoryId: string) {
        let categories = this.currentCategoriesSubject.value;
        for (var i = 0; i < categories.length; i++) {
            if (categories[i].id === categoryId) {
                categories.splice(i, 1);
            }
        }

        this.currentCategoriesSubject.next(categories);

        if (!categories.length) return;

        this.currentCategorySubject.next(categories[0].id);
    }
}

The part I need to mock is the public property category. I am not sure how to go about mocking that. Does anyone have any idea?

This is the ngOnInit method for the component I am testing:

ngOnInit() {
    console.log(this.selectorService);

    this.selectorService.category.subscribe(category => {
        if (!category) return;

        this.fieldService.list(category, false).subscribe(fields => {
            let rangeFields = fields.filter(field => !field.isSpecification);
            let specificationFields = fields.filter(field => field.isSpecification);

            this.fields = specificationFields;

            this.productService.listValidatedRangeProducts(category).subscribe(range => {
                if (!range.length) return;
                this.products = range;
                this.range = this.productModelService.mapProducts(rangeFields, range);
                this.saveForm = this.toFormGroup(range[0]);
            });
        });
    });
}

private toFormGroup(product: any): FormGroup {
    let group: any = {};

    this.fields.forEach(field => {
        group[field.name] = new FormControl(product[field.name]);
    });

    return new FormGroup(group);
}

Solution

  • I was able to fix this by using the first option:

    import { Spectator, createComponentFactory, mockProvider } from '@ngneat/spectator';
    import { HttpClientTestingModule } from '@angular/common/http/testing';
    import { ReactiveFormsModule } from '@angular/forms';
    import { of } from 'rxjs';
    
    import { ToastrModule } from 'ngx-toastr';
    import { SharedModule } from '@shared';
    import { SelectorService, ProductModelService } from '@core';
    import { FieldService, ProductService } from '@services';
    import { SpecificationsSaveComponent } from './specifications-save.component';
    
    describe('SpecificationsSaveComponent', () => {
        const SelectorServiceMock = {
            category: of('cameras'),
        };
    
        const FieldServiceMock = {
            list: (categoryId: string, specificationOnly: boolean) =>
                of([
                    {
                        id: 0,
                        canCopy: false,
                        categoryId: 'cameras',
                        dataType: 0,
                        display: false,
                        isSpecification: true,
                        name: 'gtin',
                        order: 0,
                        required: true,
                    },
                ]),
        };
    
        const ProductServiceMock = {
            listValidatedRangeProducts: (categoryId: string) =>
                of([
                    {
                        gtin: 0,
                    },
                    {
                        gtin: 1,
                    },
                ]),
        };
    
        let spectator: Spectator<SpecificationsSaveComponent>;
        const createComponent = createComponentFactory({
            component: SpecificationsSaveComponent,
            imports: [ReactiveFormsModule, HttpClientTestingModule, SharedModule, ToastrModule.forRoot()],
            providers: [
                mockProvider(SelectorService, SelectorServiceMock),
                mockProvider(FieldService, FieldServiceMock),
                mockProvider(ProductService, ProductServiceMock),
            ],
        });
    
        beforeEach(() => {
            spectator = createComponent();
        });
    
        it('should create', () => {
            expect(spectator.component).toBeTruthy();
        });
    });
    

    I don't know why the second option would not work. I could see that the selector service had the correct value, but for some reason it was not being passed to the ngOnInit method. Can someone explain why these two lines don't work?

    beforeEach(() => {
        spectator = createComponent();
        spectator.get(SelectorService).category = of('cameras');
    });