it('should render title', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.nativeElement;
let language = 'en';
let title = 'Welcome to ...';
if (localStorage.getItem('language')) {
language = localStorage.getItem('language')!;
} else if (navigator.language) {
language = navigator.language.slice(0, 2);
} else {
language = 'en';
}
if (language === 'en') {
title = 'Welcome to ...';
} else if (language === 'de') {
title = 'Willkomen zu ...';
}
expect(compiled.querySelector('#title')?.textContent).toContain(title);
});
class AppConfigServiceStub {
data = {
"title": "Willkomen zu ...",
"language": "de",
"server": {
"host": "localhost:",
"port": "8000"
}
};
// load(defaults?: AppConfig): Promise<AppConfig> {
// let data: AppConfig | any = {};
// return new Promise<AppConfig>(resolve => {
// data = Object.assign({}, defaults || {}, this.data || {});
// resolve(this.data);
// }
// );
// }
}
class TranslateServiceStub {
data = {
"title": "Willkomen zu ...",
"Language": "Sprache",
"Change language:": "Sprache ändern:"
}
}
#Angular Testing Jest mocking translate pipe
43 messages · Page 1 of 1 (latest)
Then don't use the stub? And setup HttpTestingController to mock the responses that AppConfigService expects?
But what I see of your AppConfigServiceStub makes me thing that your AppConfigService should be more Observable-y
the appConfigService.ts
@Injectable({
providedIn: 'root'
})
export class AppConfigService {
data: AppConfig | any = {};
constructor(private http: HttpClient) { }
load(defaults?: AppConfig): Promise<AppConfig> {
return new Promise<AppConfig>(resolve => {
this.http.get('app.config.json').subscribe({
next: response => {
console.log('using server-side configuration');
this.data = Object.assign({}, defaults || {}, response || {});
resolve(this.data);
},
error: error => {
console.error(error);
console.log('using default configuration');
this.data = Object.assign({}, defaults || {});
resolve(this.data);
}
});
});
}
use(lang: string): Promise<Record<string, unknown>> {
return new Promise<Record<string, unknown>>((resolve) => {
const langPath = `./assets/languages/${lang || 'en'}.json`;
this.http.get<Record<string, unknown>>(langPath).subscribe({
next: response => {
this.data = Object.assign({}, response || {});
console.log(this.data);
resolve(this.data);
},
error: err => {
console.error(err);
this.data = {};
resolve(this.data);
}
}
);
});
}
}
g!any @civic crater
@civic crater Try to avoid using any as a type. The TS compiler effectively treats any as “please turn off type checking for this thing”. https://www.typescriptlang.org/docs/handbook/declaration-files/do-s-and-don-ts.html#any
Always prefer to supply the proper type. You will benefit from the confidence that static typing brings, along with an enhanced developer experience through tooling (e.g. intellisense, refactoring, etc.).
In cases where you don’t know what type you want to accept, or when you want to accept anything because you will be blindly passing it through without interacting with it, you can use the unknown type. unknown introduction:https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-0.html#new-unknown-top-type and some practical examples: https://mariusschulz.com/blog/the-unknown-type-in-typescript.
const DEFAULT_CONFIG: AppConfig = {
// ???
} as const;
@Injectable({ providedIn: 'root' })
export class AppConfigService {
public readonly data$: Observable<AppConfig>;
constructor(private readonly http: HttpClient) {
this.data$ = this.http.get<AppConfig>('app.config.json').pipe(
map((serverConfig: AppConfig): AppConfig => {
return {
...DEFAULT_CONFIG,
...serverConfig,
};
}),
// Replay the most recent (bufferSize) emission on each subscription
// Keep the buffered emission(s) (refCount) even after everyone unsubscribes. Can cause memory leaks.
shareReplay({ bufferSize: 1, refCount: false }),
);
}
}
translateService.ts
@Injectable({
providedIn: 'root'
})
export class TranslateService {
data: Record<string, unknown> = {}
constructor(private http: HttpClient) { }
use(lang: string): Promise<Record<string, unknown>> {
return new Promise<Record<string, unknown>>((resolve) => {
const langPath = `./assets/languages/${lang || 'en'}.json`;
this.http.get<Record<string, unknown>>(langPath).subscribe({
next: response => {
this.data = Object.assign({}, response || {});
config.language = lang;
console.log(config.language);
resolve(this.data);
},
error: err => {
console.error(err);
this.data = {};
resolve(this.data);
}
}
);
});
}
}
and in app.module.ts
I am loading the appConfig
export function setupAppConfigServiceFactory(
service: AppConfigService) {
return () => service.load();
}
export function setupTranslateServiceFactory(
service: TranslateService) {
return () => {
try{
if(localStorage.getItem('language')){
service.use(localStorage.getItem('language')!);
}else if(navigator.language){
service.use(navigator.language);
}else{
service.use('en');
}
} catch(err){
console.error(err);
}
}
}
@NgModule({
declarations: [
AppComponent,
TreeviewComponent,
TranslatePipe
],
imports: [
CommonModule,
BrowserAnimationsModule,
BrowserModule,
AppRoutingModule,
HttpClientModule,
MaterialModule,
],
providers: [
TranslateService,
{
provide: APP_INITIALIZER,
useFactory: setupAppConfigServiceFactory,
deps: [
AppConfigService
],
multi: true
},
{
provide: APP_INITIALIZER,
useFactory: setupTranslateServiceFactory,
deps: [
TranslateService
],
multi: true
}],
bootstrap: [AppComponent]
})
export class AppModule { }
why do I have to put the three dots in return?
The spread (...) syntax allows an iterable, such as an array or string, to be expanded in places where zero or more arguments (for function calls) or elements (for array literals) are expected. In an object literal, the spread syntax enumerates the properties of an object and adds the key-value pairs to the object being created.
It merges the objects into a new object.
It is the modern version of this.data = Object.assign({}, defaults || {}, response || {});
ah cool.
i changed the appConfigService now to this:
@Injectable({
providedIn: 'root'
})
export class AppConfigService {
// data: AppConfig | any = {};
DEFAULT_CONFIG: AppConfig = {
title: 'Welcome to Bosch Data Editor NT',
language: 'en'
} as const;
public readonly data$: Observable<AppConfig>;
public translation$!: Observable<Record<string, unknown>>;
constructor(private http: HttpClient) {
this.data$ = this.http.get<AppConfig>('app.config.json').pipe(
map((serverConfig: AppConfig): AppConfig => {
return {
...serverConfig,
...this.DEFAULT_CONFIG
};
}),
// Replay the most recent (bufferSize) emission on each subscription
// Keep the buffered emissions (refcount) even after everyone unsubscribes to avoid memory leaks.
share(),);
}
use(lang: string): Observable<Record<string, unknown>> {
let translation = {}
const langPath = `./assets/languages/${lang || 'en'}.json`;
return this.translation$ = this.http.get<Record<string, unknown>>(langPath).pipe(
map((translated: Record<string, unknown>): Record<string, unknown> => {
return {
...translated,
...{}
};
}),
shareReplay({ bufferSize: 1, refCount: false }),);
}
}
but if I console.log the appCOnfig data I get the data$ and DEFAULT_CONFIG
This: ${lang || 'en'} is worrisome. Why would it be falsey?
and if I subscribe the observable and console log .data it is the content of the default config
the 'en' is just a fallback for default language
data$ is an Observable, you have to subscribe to it to get values out of it. In Angular you would chain that together with other Observables using the .pipe and then subscribe using the AsyncPipe (| async) in your Component template
use(lang: string = 'en'): Observable<
ah I discovered my mistake. I accidently switched order of return in the map
so instead of first returning defaultconfig I returned the serverconfig
Oh that is what you meant. Good catch
does this look good?
export function setupAppConfigServiceFactory(
service: AppConfigService) {
return () => {
try {
if (service.data$) {
console.log('using server-side configuration');
service.data$.subscribe(data => {
return data;
}
);
} else {
console.log('using default configuration');
return service.DEFAULT_CONFIG;
}
} catch (err) {
console.error(err);
}
}
}
oops, Not all code paths return a value
I can't return in catch defaultConfig
maybe I don't need try and catch
I don't know if it makes sense if I return err in catch since the app should load the server config and if not existing then take the defaultconfig
You are making your life difficult by not just accepting the Observable patterns primarily used in Angular.
@Component({
template: `
<main *ngIf="vm$ | async; else loading">
</main>
<ng-template #loading>Loading...</ng-template>
`,
})
export class AppComponent {
public readonly vm$: Observable<unknown>; // Don't actually need a ViewModel in this example, but if you did that would be the type.
constructor(
private readonly configService: AppConfigService,
private readonly titleService: Title,
) {
this.vm$ = this.configService.data$.pipe(
tap((config: AppConfig): void => {
this.titleService.setTitle(config.theTitleOrWhatever);
}),
);
}
}
how can I mock the service where there is no method with spy? in this case the observable gets stuffed in the constructor. if I want to mock the service in the component. How do I do that?
I find the angular documentation concerning testing a bit hard to understand. Is there better tutorials or videos you can recommend?
let mockConfigSub$: Subject<AppConfig>;
beforeEach(async () => {
mockConfigSub$ = new Subject<AppConfig>();
await TestBed.configureTestingModule({
providers: [ { provide: AppConfigService, useValue: { data$: mockConfigSub$ } } ],
thanks man
@runic locust so here we mock just the observable data$ from the appConfigService right?
I am using this line to access the id of an html div element
expect(compiled.querySelector('#title')?.textContent).toContain(title);
but I get
Expected 'title' to contain 'Willkommen zu ...'
is something wrong in the syntax to get the text value of the id of the div element?
There seems to be a typo in the title string that you expect in the test. You have written 'Willkomen' instead of 'Willkommen'.
You can try changing this line:
let title = 'Willkomen zu ...';
to:
let title = 'Willkommen zu ...';
This should fix the test failure.
thanks for the typo catch, but I still get the same error.
Maybe because its
{{'title' | translate}}
since it uses translation pipe
Angular Testing Jest mocking translate pipe
I updated my test concerning the translation and get the following error:
FAIL src/app/app.component.spec.ts
● AppComponent › should render title
expect(received).toContain(expected) // indexOf
Expected substring: "Willkommen zu ..."
Received string: " title "
104 | // title = 'Willkommen zu ...';
105 | // }
> 106 | expect(compiled.querySelector('#title')?.textContent).toContain(TRANSLATIONS_DE.title);
| ^
107 | });
this is the part in the test file:
describe('AppComponent', () => {
let httpClient: HttpClient;
let httpTestingController: HttpTestingController;
let mockAppConfigService: AppConfigService;
let mockTranslateService: TranslateService;
let mockAppConfigSub$: Subject<AppConfig>;
const TRANSLATIONS_DE = require('../assets/languages/de.json');
const TRANSLATIONS_EN = require('../assets/languages/en.json');
beforeEach(async () => {
mockAppConfigSub$ = new Subject<AppConfig>();
await TestBed.configureTestingModule({
imports: [
RouterTestingModule,
HttpClientTestingModule,
],
providers: [
{ provide: mockAppConfigService, useValue: { data$: mockAppConfigSub$ } },
// TranslateService,
// AppConfigService
],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
declarations: [
AppComponent,
TranslatePipe
],
}).compileComponents();
// Inject the http service and test controller for each test
httpClient = TestBed.inject(HttpClient);
httpTestingController = TestBed.inject(HttpTestingController);
mockAppConfigService = TestBed.inject(AppConfigService);
mockTranslateService = TestBed.inject(TranslateService);
// spyOn(mockTranslateService, 'use').and.returnValue();
});
...
it('should render title', () => {
let language = 'en';
let title = 'Welcome to ...';
const fixture = TestBed.createComponent(AppComponent);
const compiled = fixture.nativeElement;
mockTranslateService.use('de');
fixture.detectChanges();
expect(compiled.querySelector('#title')?.textContent).toContain(TRANSLATIONS_DE.title);
});