Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to unit test a component with TranslateService and the translate pipe? #636

Closed
Ks89 opened this issue Aug 15, 2017 · 34 comments
Closed

Comments

@Ks89
Copy link

Ks89 commented Aug 15, 2017

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]

@kartheininger
Copy link

kartheininger commented Aug 17, 2017

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 {
		return 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.

@erhimanshugarg
Copy link

Can anyone suggest on my issue #627

@Ks89
Copy link
Author

Ks89 commented Aug 17, 2017

@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.

@Ks89
Copy link
Author

Ks89 commented Aug 17, 2017

@kartheininger Also, adding both TranslatePipe to my declarations and your mock I get this:

Failed: this.translate.get(...) is undefined

@kartheininger
Copy link

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 #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 :)

@Ks89
Copy link
Author

Ks89 commented Aug 19, 2017

@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.

@Ks89
Copy link
Author

Ks89 commented Aug 20, 2017

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.

@kartheininger
Copy link

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 :))

@Zhichao-Hong
Copy link

@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.

@shairez
Copy link

shairez commented Sep 28, 2017

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).

@pherris
Copy link

pherris commented Oct 6, 2017

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...

@xmeng1
Copy link

xmeng1 commented Jan 19, 2018

how to load translate JSON file in karma?

@ronjoy4
Copy link

ronjoy4 commented Feb 8, 2018

@pherris Nice work! Spent so much time trying to get that damn translatemodule to work.

@kgish
Copy link

kgish commented Feb 9, 2018

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?

@ronjoy4
Copy link

ronjoy4 commented Feb 9, 2018

@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.

@kgish
Copy link

kgish commented Feb 9, 2018

Yes indeed.

Based on #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.

@ap1969
Copy link

ap1969 commented Mar 17, 2018

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)

@leemon20
Copy link

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

mraible pushed a commit to oktadev/ionic-jhipster-starter that referenced this issue Apr 24, 2018
@ocombe
Copy link
Member

ocombe commented May 8, 2018

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 ocombe closed this as completed May 8, 2018
@gclark-sieff
Copy link

@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.

@ocombe
Copy link
Member

ocombe commented May 9, 2018

I've never used ionic, but maybe @danielsogl could help out?

@yavin5
Copy link

yavin5 commented Aug 10, 2018

I can verify that Oliver (ocombe)'s test example code worked. Thanks Oliver!!

@kgish
Copy link

kgish commented Aug 13, 2018

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?

@yuezhizizhang
Copy link

Can't @kgish more. I don't want to include all of the components into my test file

@kbirger
Copy link

kbirger commented Oct 15, 2018

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?

@chaimmw
Copy link

chaimmw commented Dec 13, 2018

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>

@AndreiShostik
Copy link

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 {

}

@dsebastien
Copy link

Has this testing module been integrated into the library? Or will it be?

@ryboucher
Copy link

ryboucher commented Mar 16, 2020

@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.

@ImMortex
Copy link

ImMortex commented Mar 31, 2020

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

@vutienthinh
Copy link

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?

@Aracturat
Copy link

Aracturat commented Jul 31, 2020

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

@Gleam01
Copy link

Gleam01 commented Jun 29, 2021

Hello @Ks89 and everyone.
Hope you're doing well !

You can try ngx-translate-testing package to see if you can figure out with your problem. But you need to upgrade your project Angular version to at least version 6 as said in Installation section.

@mrctrifork
Copy link

how to load translate JSON file in karma?

I am coming quite late on this question but you can just require('path/to/en.json') and should just work if it's valid JSON

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests