Home > Back-end >  Angular/Jest - Test that the subject has thrown error after setter is called
Angular/Jest - Test that the subject has thrown error after setter is called

Time:05-13

I have a class that is responsible for setting the theme for my application.

I'm trying to test that it throws an error if you give it a theme that it does not recognise. The code is shown below -

@Injectable()
export class ThemeSwitcherService {
    public static THEME_NAME_COOKIE = 'THEME_NAME';

    private readonly _themeNameBehaviourSubject$: BehaviorSubject<string>;
    private readonly _document: Document;
    private readonly _themes: Map<string, Map<string, string>>;

    constructor(@Inject(DOCUMENT) document: Document, themeConfig: ApplicationThemeConfig) {
        this._themeNameBehaviourSubject$ = new BehaviorSubject<string>('');
        this._document = document;
        this._themes = themeConfig.moduleThemes;

        this.initialise();
    }

    public get currentThemeNameSubscription(): Observable<string> {
        return this._themeNameBehaviourSubject$.asObservable();
    }

    public set themeName(themeName: string) {
        this._themeNameBehaviourSubject$.next(themeName);
    }

    private setTheme(themeName: string): void {
        const selectedTheme: Map<string, string> | undefined = this._themes.get(themeName);

        if (typeof selectedTheme === 'undefined') {
            throw new Error(`Could not find theme named ${ themeName }. Please add ${ themeName } to your forRoot configuration`);
        }

        selectedTheme.forEach((value: string, key: string) => {
            const root = this._document.body;
            root.style.setProperty(key, value);
        });
    }

    private initialise(): void {
        const storedTheme = Cookies.get(ThemeSwitcherService.THEME_NAME_COOKIE);

        if (storedTheme) {
            this.themeName = storedTheme;
        }

        if (!storedTheme) {
            const firstThemeAvailable = this._themes.entries().next().value;
            this.themeName = firstThemeAvailable[0];
        }

        this.subscribeToThemeNameSubject();
    }

    private subscribeToThemeNameSubject(): void {
        this._themeNameBehaviourSubject$
            .pipe(tap((themeName: string) => Cookies.set(ThemeSwitcherService.THEME_NAME_COOKIE, themeName)))
            .subscribe({
                next: (themeName: string) => this.setTheme(themeName),
                error: (error) => console.error(error)
            });
    }
}

when the themeName setter is called with a value, it updates a behaviour subject and calls the private setTheme(themeName: string) method, this throws an error if the theme is unknown.

I'm trying to test this line of code:

if (typeof selectedTheme === 'undefined') {
            throw new Error(`Could not find theme named ${ themeName }. Please add ${ themeName } to your forRoot configuration`);
        }

I have this so far:


import { ThemeSwitcherService } from './theme-switcher.service';
import { ApplicationThemeConfig } from '../setup/application-theme.config';
import { TestBed } from '@angular/core/testing';
import { ApplicationThemeConfigurationFactory } from '../../../config/theme/factories/application-theme-configuration.factory';
import { ThemeModule } from '../theme.module';
import DoneCallback = jest.DoneCallback;

describe('ThemeSwitcherService', () => {
    let subject: ThemeSwitcherService;

    describe('Given a theme that does not exist', () => {

        beforeEach(() => {
            TestBed.configureTestingModule({
                imports: [ThemeModule],
                providers: [
                    {
                        provide: ApplicationThemeConfig,
                        useValue: {
                            moduleThemes: ApplicationThemeConfigurationFactory.getThemes()
                        }
                    }
                ]
            });

            subject = TestBed.inject<ThemeSwitcherService>(ThemeSwitcherService);
        });

        describe('When the theme is selected', () => {
            it('Then an error is thrown', (done: DoneCallback) => {
                subject.themeName = 'Something';
                subject.currentThemeNameSubscription
                    .subscribe((currentTheme) => {
                        expect(currentTheme).toThrowError();
                        done();
                    });

            });
        });
    });
});

But I keep getting the error:

Error: Could not find theme named Something. Please add Something to your forRoot configuration

Which is what I'd expect but I'd also expect my unit test to pass. I'm not sure what I'm doing wrong.

CodePudding user response:

The throw new Error isn't part of an observale flow so it won't be captured by the subscription to currentThemeNameSubscription

Quickest option is wrapping the call to this.setTheme(themeName) in a regular try/catch block and call an error handling method (spy on the error handling method in your test)

I'm not sure of the purpose of the BehaviorSubject in this case tbh

CodePudding user response:

As @Drenai said, there was no need for the BehaviourSubject. I've removed the behaviour subject and the service has become clean and should be easy to test.

Refactor:

@Injectable()
export class ThemeSwitcherService {
    public static THEME_NAME_COOKIE = 'THEME_NAME';

    private _currentThemeName: string;
    private readonly _document: Document;
    private readonly _themes: Map<string, Map<string, string>>;

    constructor(@Inject(DOCUMENT) document: Document, themeConfig: ApplicationThemeConfig) {
        this._document = document;
        this._themes = themeConfig.moduleThemes;

        this.initialise();
    }

    public get currentThemeName(): string {
        return this._currentThemeName;
    }

    public setTheme(themeName: string): void {
        const selectedTheme: Map<string, string> | undefined = this._themes.get(themeName);

        if (typeof selectedTheme === 'undefined') {
            throw new Error(`Could not find theme named ${ themeName }. Please add ${ themeName } to your forRoot configuration`);
        }

        selectedTheme.forEach((value: string, key: string) => {
            const root = this._document.body;
            root.style.setProperty(key, value);
        });

        Cookies.set(ThemeSwitcherService.THEME_NAME_COOKIE, themeName);
    }

    private initialise(): void {
        const storedTheme = Cookies.get(ThemeSwitcherService.THEME_NAME_COOKIE);

        if (storedTheme) {
            this._currentThemeName = storedTheme;
        }

        if (!storedTheme) {
            const firstThemeAvailable = this._themes.entries().next().value;
            this._currentThemeName = firstThemeAvailable[0];
        }

        this.setTheme(this._currentThemeName);
    }
}
  • Related