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);
}
}