I'm submitting a ... (check one with "x")
[ ] bug report => check the FAQ and search github for a similar issue or PR before submitting
[x] support request => check the FAQ and search github for a similar issue before submitting
[ ] feature request
Question
How to test a component that uses TranslateService and the translate pipe?
I have a root module with:
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: HttpLoaderFactory,
deps: [HttpClient]
}
})
// AoT requires an exported function for factories
export function HttpLoaderFactory(httpClient: HttpClient) {
return new TranslateHttpLoader(httpClient, "assets/i18n/", ".json");
}
also, inside my main component (with router-outlet) I have:
export class AppComponent {
constructor(private translate: TranslateService) {
translate.addLangs(["en", "it"]);
translate.setDefaultLang('en');
let browserLang = translate.getBrowserLang();
translate.use(browserLang.match(/en|it/) ? browserLang : 'en');
}
}
and finally, in my Component that I want to test I have this:
export class AboutComponent implements OnInit, OnDestroy {
pageHeader: any = { title:'', strapline: '' };
private i18nSubscription: Subscription;
constructor(private translate: TranslateService) {}
ngOnInit() {
this.i18nSubscription = this.translate.get('ABOUT')
.subscribe((res: any) => {
this.pageHeader = {
title: res['TITLE'],
strapline: res['STRAPLINE']
};
});
}
ngOnDestroy() {
if (this.i18nSubscription) {
this.i18nSubscription.unsubscribe();
}
}
}
Now I want to test 'AboutComponent'. But how?
At the moment I have this:
beforeEach( async(() => {
TestBed.configureTestingModule({
imports: [ TranslateModule.forRoot() ],
declarations: [ AboutComponent, PageHeaderComponent ]
});
fixture = TestBed.createComponent(AboutComponent);
comp = fixture.componentInstance;
fixture.detectChanges();
return fixture.whenStable().then(() => fixture.detectChanges());
}));
Obviously I have to say to my test that I'm using 2 languages (en and it), but I don't know which is the right way to do that.
At the moment both TemplateService and pipes aren't working.
For instance, I created this to test the result of a traslate pipe:
it('should display the about page', () => {
const element: DebugElement = fixture.debugElement;
fixture.detectChanges();
const message: DebugElement[] = element.queryAll(By.css('small'));
expect(message.length).toBe(2);
expect(message[0].nativeElement.textContent.trim()).toBe('');
expect(message[1].nativeElement.textContent.trim()).toBe('Not implemented yet');
});
and the result is this error:
Expected 'ABOUT.NOT_IMPLEMENTED' to be 'Not implemented yet'.
Thank u.
Please tell us about your environment:
ngx-translate version: 7.2.0
Angular version: 4.3.4
Browser: [all]
If you unit test your component, then you should mock the service.
So in the spec instead of importing TranslateModule you should add this to your providers:
{provide: TranslateService, useClass: TranslateServiceStub}
And then do something like this:
export class TranslateServiceStub{
public get(key: any): any {
Observable.of(key);
}
}
or you return the key with a suffix, so you know "the service was called".
Because when you unit test your component, you should rely on that external dependencies (like the translateService) are already tested and they are working.
Can anyone suggest on my issue #627
@kartheininger If I do that I receive this error
The pipe 'translate' could not be found
because I'm using both TranslateService and pipe.
To be more accurate:
I don't wanna test ngx-translate, but that it is applying the correct translation. Obviously I don't want to show "BLABLA.BLALA" instead of "Hello".
I saw while developing that if you make a stupid error all translations of the page will be broken, so I want to be sure that everything is working in the right way.
In fact, I want to test that my component is shown with the right labels also changing the language. So, I'm not testing ngx-translate.
@kartheininger Also, adding both TranslatePipe to my declarations and your mock I get this:
Failed: this.translate.get(...) is undefined
Have you tried to use the real pipe and the mock i suggested together? I don`t know if thats working, because in our app we mocked both in tests.
If you use the pipe too, then i would add a mock for the pipe too. I copied a small example within https://github.com/ngx-translate/core/issues/635
I personally would just let the mocks return the key a bit modified e.g. with a _1_ at the end. So you know the pipe/service was called. I hope i got you right and thats what you wanna test :)
@kartheininger
Have you tried to use the real pipe and the mock i suggested together? I don`t know if thats working, because in our app we mocked both in tests.
yes. Using Service mock and the real pipe I get this error: "Failed: this.translate.get(...) is undefined"
Using the Pipe mock doesn't change anything.
My problem is that I don't wanna use mocks. I can't test that my components are ok, without to know that translations are correct.
I don't wanna test TranslateService or other, but that they are applying the right translations in my component.
I want to be sure that all texts are defined in all files and that are applied in the right place. I cannot release a multi language application in production without to now that all label, texts and so on are working very well.
That because I use strings to get translations (for instance "ABOUT.TITLE") and a simple typo could break translation and my app will show a wrong label. That is a big problem in production.
I think that this is a use case where mocks are very bad. It should be possibile to apply real translations. Only with real translations I will be sure that everything will be ok in production.
I updated your mock because you forgot to add "return":
export class TranslateServiceStub{
public get(key: any): any {
return Observable.of(key);
}
}
but now I'm receiving:
Failed: this.translate.onTranslationChange is undefined
It isn't so simple to mock this service.
If that method in your test is missing, then you need to add it to the Stub :)
It should probably return the same type then the real implementation, therefore have a look here: https://github.com/ngx-translate/core/blob/master/src/translate.service.ts
You need to mock all the methods you are calling :)
In our tests we do not need more methods mocked, as the user cannot change his language without leaving the app and reloading it (user settings are done in a different system :))
@kartheininger Any example mock that you are using in your. I am having problem with TranslateDirective. Do I need to mock this too? It will be nice to include a unit testing example project in your examples folder.
By the way, to save you time of mocking manually everything
you can use something like this library I wrote -
https://github.com/hirezio/jasmine-auto-spies
Where you could do something like -
translateServiceSpy = createSpyFromClass(TranslateService, null, ['get', 'use', 'anyOtherObservableMethod')
and get a simple API of -
translateServiceSpy.get.and.nextWith(anyValue)
It will also auto mock your sync methods as well (without having to specify them).
I'm just coming up to speed on this stack and was trying to wrap some tests around an Ionic component. I ran across this same problem and solved it by looking at translate.service.spec.ts (https://github.com/ngx-translate/core/blob/master/tests/translate.service.spec.ts) and the comments in this thread.
Essentially I had to define TranslateModule in imports and provide it a TranslateLoader called FakeLoader which wrapped my manually defined translations (it'd be nice to use the actual file here). Next I had to get the TranslateService instance and set the language to use in my test.
import { ComponentFixture, TestBed, getTestBed } from '@angular/core/testing';
import {Injector} from "@angular/core";
import { IonicModule, Platform, NavController } from 'ionic-angular';
import { TranslateModule, TranslateLoader, TranslateService } from '@ngx-translate/core';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import {Observable} from 'rxjs/Observable';
import { CardsPage } from './cards';
import {
PlatformMock,
NavMock,
} from '../../../test-config/mocks-ionic';
let translations: any = {"CARDS_TITLE": "This is a test"};
class FakeLoader implements TranslateLoader {
getTranslation(lang: string): Observable<any> {
return Observable.of(translations);
}
}
describe('CardsPage (inline template)', () => {
let comp: CardsPage;
let fixture: ComponentFixture<CardsPage>;
let de: DebugElement;
let el: HTMLElement;
let translate: TranslateService;
let injector: Injector;
it('true is true', () => expect(true).toBe(true));
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [CardsPage],
imports: [
IonicModule.forRoot(CardsPage),
TranslateModule.forRoot({
loader: {provide: TranslateLoader, useClass: FakeLoader},
})
],
providers: [
{ provide: Platform, useClass: PlatformMock },
{ provide: NavController, useClass: NavMock },
]
});
injector = getTestBed();
translate = injector.get(TranslateService);
fixture = TestBed.createComponent(CardsPage);
comp = fixture.componentInstance;
de = fixture.debugElement.query(By.css('ion-title'));
el = de.nativeElement;
});
it('should include the title of the cards page', () => {
translate.use('en');
fixture.detectChanges();
expect(el.textContent).toContain('This is a test')
});
});
This seems like a crazy amount of configuration and mocking for every component but I'm new to the Angular stack...
how to load translate JSON file in karma?
@pherris Nice work! Spent so much time trying to get that damn translatemodule to work.
This is a crazy amount of extra work and becomes very cumbersome if this has to be included in every test. There must be an easier way, or not?
@kgish You could probably create a custom object for the testingmodule and import it into each test?
If you had anything extra to add to the testing module, you could always extend that object via a class or dynamically.
Yes indeed.
Based on https://github.com/ngx-translate/core/issues/471, I tried extracting all of the translation-specific stuff into a separate helper module, but was unable to come up with a simple solution.
Any hints or tips would be greatly appreciated.
I've got everything I need mocked out (translate pipe and setDefaultLang) using Jest. I'll see if I can pull together a complete spec with just the translation piece in it (at the moment, it's all mixed-in with project-specific code)
Thanks to @pherris, @Ks89 and https://stackoverflow.com/a/43833423 one possible solution that checks all supported languages is.
ADVANTAGE: Uses real translation files/contents (json) to validate element contents. In case keys are change inside translation file (json) tests will fail.
ATTENTION: code most probably will not compile. serves as an example what steps are to be done.
import { async, TestBed } from '@angular/core/testing';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { TranslateModule, TranslateLoader, TranslateService } from '@ngx-translate/core';
import { NotFoundPageComponent, NotFoundPageModule } from '@app/areas/public';
import * as de from '@assets/i18n/de.json';
import * as en from '@assets/i18n/en.json';
import { NotFoundPagePage } from './not-found-page.po';
const TRANSLATIONS = {
DE: de,
EN: en
};
class JsonTranslationLoader implements TranslateLoader {
getTranslation(code: string = ''): Observable<object> {
const uppercased = code.toUpperCase();
return of(TRANSLATIONS[uppercased]);
}
}
describe('NotFoundPage:', () => {
describe('i18n:', () => {
let page: NotFoundPagePage;
let component: NotFoundPageComponent;
let service: TranslateService;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
NoopAnimationsModule,
TranslateModule.forRoot({
loader: { provide: TranslateLoader, useClass: JsonTranslationLoader },
}),
NotFoundPageModule
]
});
service = TestBed.get(TranslateService);
page = new NotFoundPagePage(TestBed);
component = page.component;
// calls internally
// detectChanges() -> whenStable().then(() => detectChanges());
return page.waitForCompoentToBecomeStable();
}));
Object.keys(TRANSLATIONS).forEach((k) => {
const key = k.toLowerCase();
describe(`${key}:`, () => {
beforeEach(() => {
service.use(key);
page.detectChanges();
});
it('should have translated value as username hint', async () => {
service.get('LOGIN_FORM.USERNAME').subscribe((translated) => {
const text = page.getUsernameInputPlaceholderText();
expect(text).toBe(translated);
});
});
it('should have translated value as password hint', () => {
service.get('LOGIN_FORM.PASSWORD').subscribe((translated) => {
const text = page.getPasswordInputPlaceholderText();
expect(text).toBe(translated);
});
});
});
});
});
});
I have added a test example here if you still need it: https://github.com/ngx-translate/example/blob/master/src/app/app.component.spec.ts
The example app uses Angular CLI v6
@ocombe would it be possible to show a test example of app.component.spec.ts in an Ionic 3 project using Karma/Jasmine combination? I get "The pipe 'translate' could not be found" errors when using your example.
I've never used ionic, but maybe @danielsogl could help out?
I can verify that Oliver (ocombe)'s test example code worked. Thanks Oliver!!
Yes, this may work but it is still quite inconvenient to have to include all of this boiler-plate code in every test that depends on ngx-translate. There must be a better option possible?
Can't @kgish more. I don't want to include all of the components into my test file
Angular provides a module HttpClientTestingModule for mocking HttpClient. Why not follow the same pattern here and provide this as a first-party module for testability of your project?
I was able to run tests with configuration mentioned above, but, on one of my test's, when I used the pipe I got this error "Error: Parameter "key" required" but when I switched to directive it did work?
// does not work
<span class="status" *ngIf="status" [ngClass]="{
idle: data.status === 'idle',
on: data.status === 'on',
off: data.status === 'off',
disc: data.status === 'disc'
}">
{{data.status | translate}}
</span>
// works
<span class="status" *ngIf="status" [ngClass]="{
idle: data.status === 'idle',
on: data.status === 'on',
off: data.status === 'off',
disc: data.status === 'disc'
}" translate>
{{data.status}}
</span>
agree with @kbirger
@ocombe, please take a look
import { Injectable, NgModule, Pipe, PipeTransform } from '@angular/core';
import { TranslateLoader, TranslateModule, TranslatePipe, TranslateService } from '@ngx-translate/core';
import { Observable, of } from 'rxjs';
const translations: any = {};
class FakeLoader implements TranslateLoader {
getTranslation(lang: string): Observable<any> {
return of(translations);
}
}
@Pipe({
name: 'translate'
})
export class TranslatePipeMock implements PipeTransform {
public name = 'translate';
public transform(query: string, ...args: any[]): any {
return query;
}
}
@Injectable()
export class TranslateServiceStub {
public get<T>(key: T): Observable<T> {
return of(key);
}
}
@NgModule({
declarations: [
TranslatePipeMock
],
providers: [
{ provide: TranslateService, useClass: TranslateServiceStub },
{ provide: TranslatePipe, useClass: TranslatePipeMock },
],
imports: [
TranslateModule.forRoot({
loader: { provide: TranslateLoader, useClass: FakeLoader },
})
],
exports: [
TranslatePipeMock,
TranslateModule
]
})
export class TranslateTestingModule {
}
Has this testing module been integrated into the library? Or will it be?
@AndreiShostik
With Angular 9 and Ngx translate 12, the TranslateTestingModule does not seem to be working anymore. The TranslateModule is ignoring the provided mocked service and pipe. Is anyone else experiencing this issue?
Note: If I turn Angular Ivy off, the module works.
Hello there. One guy in my team used a mocked class and a spy. The lines with the '<====' are the most important ones. Works for Angular 8 and 9
The mocked class: i18n.pipe.mock.ts
import {Pipe, PipeTransform} from '@angular/core';
// noinspection AngularMissingOrInvalidDeclarationInModule
@Pipe({name: 'i18n'})
export class I18nPipeMock implements PipeTransform {
transform(value: number): number {
return value;
}
}
Test spec to your Component "PageErrorMessageComponent", here: page-error-message.component.spec.ts for Example. PageErrorMessageComponent麓s template uses I18 Pipe.
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import {PageErrorMessageComponent} from './page-error-message.component';
import {NO_ERRORS_SCHEMA} from '@angular/core';
import {I18nPipeMock} from '../../pipes/i18n.pipe.mock'; /*I18 Pipe Mock, refers to the mocked class. Replace with your path <====*/
describe('PageErrorMessageComponent', () => {
let component: PageErrorMessageComponent;
let fixture: ComponentFixture<PageErrorMessageComponent>;
let i18nSpy; /*<====*/
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [PageErrorMessageComponent, I18nPipeMock], /*I18 Pipe Mock <====*/
schemas: [NO_ERRORS_SCHEMA],
})
.compileComponents();
i18nSpy = spyOn(I18nPipeMock.prototype, 'transform'); /*I18 Pipe Mock <====*/
}));
beforeEach(() => {
fixture = TestBed.createComponent(PageErrorMessageComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
Hello there. One guy in my team used a mocked class and a spy. The lines with the '<====' are the most important ones. Works for Angular 8 and 9
The mocked class: i18n.pipe.mock.ts
import {Pipe, PipeTransform} from '@angular/core'; // noinspection AngularMissingOrInvalidDeclarationInModule @Pipe({name: 'i18n'}) export class I18nPipeMock implements PipeTransform { transform(value: number): number { return value; } }Test spec to your Component "PageErrorMessageComponent", here: page-error-message.component.spec.ts for Example. PageErrorMessageComponent麓s template uses I18 Pipe.
import {async, ComponentFixture, TestBed} from '@angular/core/testing'; import {PageErrorMessageComponent} from './page-error-message.component'; import {NO_ERRORS_SCHEMA} from '@angular/core'; import {I18nPipeMock} from '../../pipes/i18n.pipe.mock'; /*I18 Pipe Mock, refers to the mocked class. Replace with your path <====*/ describe('PageErrorMessageComponent', () => { let component: PageErrorMessageComponent; let fixture: ComponentFixture<PageErrorMessageComponent>; let i18nSpy; /*<====*/ beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [PageErrorMessageComponent, I18nPipeMock], /*I18 Pipe Mock <====*/ schemas: [NO_ERRORS_SCHEMA], }) .compileComponents(); i18nSpy = spyOn(I18nPipeMock.prototype, 'transform'); /*I18 Pipe Mock <====*/ })); beforeEach(() => { fixture = TestBed.createComponent(PageErrorMessageComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); });
Does anyone try it and confirm that it works?
The next code works fine for me:
import { NgModule } from '@angular/core';
import { TranslateModule } from '@ngx-translate/core';
@NgModule({
imports: [
TranslateModule.forRoot()
],
exports: [
TranslateModule
]
})
export class TranslateTestingModule {
}
Then simple import it
TestBed.configureTestingModule({
declarations: [...]
imports: [
TranslateTestingModule
]
});
Most helpful comment
I'm just coming up to speed on this stack and was trying to wrap some tests around an Ionic component. I ran across this same problem and solved it by looking at
translate.service.spec.ts(https://github.com/ngx-translate/core/blob/master/tests/translate.service.spec.ts) and the comments in this thread.Essentially I had to define
TranslateModulein imports and provide it aTranslateLoadercalledFakeLoaderwhich wrapped my manually defined translations (it'd be nice to use the actual file here). Next I had to get theTranslateServiceinstance and set the language to use in my test.This seems like a crazy amount of configuration and mocking for every component but I'm new to the Angular stack...