关于测试 Angular $localize (Angular 9) 的建议
Advice on testing Angular $localize (Angular 9)
示例回购
可以找到回购 here。
请注意,这不是一个完整的示例,它只是为了展示测试 $localize
的问题。
有2个分支:
1. master
1. enable-localize-unit-tests
- 这改变了 test.ts
和 polyfills.ts
以防止 @angular/localize/init
被导入,$localize
全局函数也被侦测
概览
我已经将一个项目从 Angular 8 升级到 9(Angular update guide), replacing any usage the I18n
service (from the ngx-translation/i18n-polyfill) with Angular's new $localize
function (documentation on this is pretty limited, here 是我能找到的最好的)。我可以 运行 本地化构建并再次在特定区域设置中提供应用程序。但是,在单元测试方面我遇到了一些障碍。
以前,当使用i18n-polyfill
时,I18n
服务可以注入组件等,如下(参见I18nPolyfillComponent
):
@Component({
selector: "app-i18-polyfill",
template: `<h4>{{ title }}</h4>
})
export class I18nPolyfillComponent {
readonly title: string = this.i18n({
id: "title",
value: "Hello World!"
});
constructor(private i18n: I18n) {}
}
这可以通过在组件中注入间谍来轻松测试:
describe("I18nPolyfillComponent", () => {
let component: I18nPolyfillComponent;
let fixture: ComponentFixture<I18nPolyfillComponent>;
let mockI18n: Spy;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ I18nPolyfillComponent ],
providers: [
{
provide: I18n,
useValue: jasmine.createSpy("I18n"),
},
],
})
.compileComponents().then(() => {
mockI18n = TestBed.inject(I18n) as Spy;
});
}));
beforeEach(() => {
fixture = TestBed.createComponent(I18nPolyfillComponent);
component = fixture.componentInstance;
mockI18n.and.callFake((def: I18nDef) => def.value);
});
it("should call i18n once", () => {
expect(mockI18n).toHaveBeenCalledTimes(1);
});
});
但是,我不确定是否可以编写类似的单元测试来测试 $localize
的使用,因为它是一个全局函数而不是可注入服务。
为了完整起见,使用 $localize
(参见 I18nLocalizeComponent
),组件看起来像这样:
@Component({
selector: "app-i18n-localize",
template: `<h4>{{ title }}</h4>
})
export class I18nLocalizeComponent {
readonly title: string = $localize `:@@title:Hello World!`;
}
测试原理
我想确保我的应用程序与 I18n
/$localize
进行适当的交互(调用正确的次数,使用正确的参数等)。如果有人不小心更改了跨单位 ID 或基本翻译值,这只会防止出现愚蠢的错误。
我试过的
我试图用 test.ts
中的间谍替换全局 $localize
函数并避免导入 @angular/localize/init
:
import Spy = jasmine.Spy;
import createSpy = jasmine.createSpy;
const _global: any = typeof global !== "undefined" && global;
_global.$localize = createSpy("$localize");
declare global {
const $localize: Spy;
}
然后在测试中使用间谍 $localize
(参见:
describe("I18nLocalizeComponent", () => {
let component: I18nLocalizeComponent;
let fixture: ComponentFixture<I18nLocalizeComponent>;
let mockI18n: Spy;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ I18nLocalizeComponent ],
})
.compileComponents();
}));
beforeEach(() => {
$localize.calls.reset();
$localize.and.returnValue("Hello World!);
fixture = TestBed.createComponent(I18nLocalizeComponent);
component = fixture.componentInstance;
});
it("should call $localize once", () => {
expect($localize).toHaveBeenCalledTimes(1);
});
});
间谍确实有效,但如果组件或其他组件在其模板中使用 i18n
指令,例如 (I18nLocalizeTemplateComponent
):
,测试将失败
<p i18n>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean consequat.</p>
这将失败并出现以下错误:
TypeError: Cannot read property 'substr' of undefined
at <Jasmine>
at removeInnerTemplateTranslation (http://localhost:9877/_karma_webpack_/node_modules/@angular/core/__ivy_ngcc__/fesm2015/core.js:34560:1)
at getTranslationForTemplate (http://localhost:9877/_karma_webpack_/node_modules/@angular/core/__ivy_ngcc__/fesm2015/core.js:34582:1)
at i18nStartFirstPass (http://localhost:9877/_karma_webpack_/node_modules/@angular/core/__ivy_ngcc__/fesm2015/core.js:34771:1)
at ɵɵi18nStart (http://localhost:9877/_karma_webpack_/node_modules/@angular/core/__ivy_ngcc__/fesm2015/core.js:34718:1)
at ɵɵi18n (http://localhost:9877/_karma_webpack_/node_modules/@angular/core/__ivy_ngcc__/fesm2015/core.js:35450:1)
at I18nLocalizeTemplateComponent_Template (ng:///I18nLocalizeTemplateComponent.js:15:9)
at executeTemplate (http://localhost:9877/_karma_webpack_/node_modules/@angular/core/__ivy_ngcc__/fesm2015/core.js:11949:1)
at renderView (http://localhost:9877/_karma_webpack_/node_modules/@angular/core/__ivy_ngcc__/fesm2015/core.js:11735:1)
at renderComponent (http://localhost:9877/_karma_webpack_/node_modules/@angular/core/__ivy_ngcc__/fesm2015/core.js:13244:1)
at renderChildComponents (http://localhost:9877/_karma_webpack_/node_modules/@angular/core/__ivy_ngcc__/fesm2015/core.js:11538:1)
Error: Expected undefined to be truthy.
at <Jasmine>
at UserContext.<anonymous> (http://localhost:9877/_karma_webpack_/src/app/i18n-localize-template/i18n-localize-template.component.spec.ts:23:23)
at ZoneDelegate.invoke (http://localhost:9877/_karma_webpack_/node_modules/zone.js/dist/zone-evergreen.js:364:1)
at ProxyZoneSpec.push../node_modules/zone.js/dist/zone-testing.js.ProxyZoneSpec.onInvoke (http://localhost:9877/_karma_webpack_/node_modules/zone.js/dist/zone-testing.js:292:1)
有没有人对如何处理这个用例有任何建议,或者目前不支持它?我还没有看到任何对此有帮助的文档、教程或文章,因此非常感谢任何帮助。
要在示例存储库中重现上述问题,您必须:
- 取消注释
test.ts
中的代码
- 在
polyfills.ts
中注释掉从 @angular/localize/init
导入
- 运行 测试
I18nLocalizeTemplateComponent
可以使用 npm run test -- --include src/app/i18n-localize-template/i18n-localize-template.component.spec.ts
执行测试
或者,使用 enable-localize-unit-tests
分支,然后执行步骤 3。
备注
- 根据 Angular 升级指南,以下内容已添加到项目的
polyfills.ts
中:
import "@angular/localize/init";
- 我目前正在使用 Karma (
4.4.1
) 和 Jasmine (3.5.9
) 进行单元测试
环境详细信息
_ _ ____ _ ___
/ \ _ __ __ _ _ _| | __ _ _ __ / ___| | |_ _|
/ △ \ | '_ \ / _` | | | | |/ _` | '__| | | | | | |
/ ___ \| | | | (_| | |_| | | (_| | | | |___| |___ | |
/_/ \_\_| |_|\__, |\__,_|_|\__,_|_| \____|_____|___|
|___/
Angular CLI: 9.0.6
Node: 13.2.0
OS: darwin x64
Angular: 9.0.6
... animations, cli, common, compiler, compiler-cli, core, forms
... language-service, localize, platform-browser
... platform-browser-dynamic, router
Ivy Workspace: Yes
Package Version
------------------------------------------------------------
@angular-devkit/architect 0.900.6
@angular-devkit/build-angular 0.900.6
@angular-devkit/build-ng-packagr 0.900.6
@angular-devkit/build-optimizer 0.900.6
@angular-devkit/build-webpack 0.900.6
@angular-devkit/core 9.0.6
@angular-devkit/schematics 9.0.6
@angular/cdk 9.1.3
@ngtools/webpack 9.0.6
@schematics/angular 9.0.6
@schematics/update 0.900.6
ng-packagr 9.0.3
rxjs 6.5.4
typescript 3.7.5
webpack 4.41.2
我找到的解决方案是监视 $localize
的 translate
函数而不是 $localize
本身:
test.ts
const _global: any = typeof global !== "undefined" && global;
const defaultFakedLocalizeTranslate: (messageParts: TemplateStringsArray,
substitutions: readonly any[]) => [TemplateStringsArray, readonly any[]] =
(messageParts: TemplateStringsArray, substitutions: readonly any[]) => [messageParts, substitutions];
_global.mockLocalize = createSpy("mockLocalize") as Spy;
declare global {
const mockLocalize: Spy;
}
$localize.translate = mockLocalize.and.callFake(defaultFakedLocalizeTranslate);
确保您已将 @angular/localize/init
导入 test.ts
。
I18nLocalizeComponent
单元测试可以更新如下:
describe('I18nLocalizeComponent', () => {
let component: I18nLocalizeComponent;
let fixture: ComponentFixture<I18nLocalizeComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ I18nLocalizeComponent ],
})
.compileComponents();
}));
beforeEach(() => {
mockLocalize.calls.reset();
fixture = TestBed.createComponent(I18nLocalizeComponent);
component = fixture.componentInstance;
});
it('should call $localize once', () => {
expect(mockLocalize).toHaveBeenCalledTimes(1);
});
});
此更改还将允许对模板中带有 i18n
标记的组件进行单元测试,以 运行 成功。
要检查更改,请参阅示例存储库的 fix-localize-unit-tests
分支。
找不到名字 'global'
对于在 Angular 12.运行 尝试 运行 的人。
我有以下问题
为了制作演示 运行ning 我必须添加以下内容
-> tsconfig.spec.json
将“节点”添加到编译器选项。
###tsconfig.spec.json
"compilerOptions": {
types": [
"jasmine",
"node" <====
]}
这个答案扩展了原来 post 中的测试。
任何想要测试的人
$localize 翻译了正确的字符串?
测试结果如下:
it('should call $localize.translate with "string to translate"', () => {
expect(mockLocalize.calls.argsFor(0)[0][0]).toContain('string to translate');
});
这里是Repo.
Repo 是此 post.
中给出的 repo 的“分支”
差异:
- 更新于 Angular 12
- 添加了翻译字符串测试
示例回购
可以找到回购 here。
请注意,这不是一个完整的示例,它只是为了展示测试 $localize
的问题。
有2个分支:
1. master
1. enable-localize-unit-tests
- 这改变了 test.ts
和 polyfills.ts
以防止 @angular/localize/init
被导入,$localize
全局函数也被侦测
概览
我已经将一个项目从 Angular 8 升级到 9(Angular update guide), replacing any usage the I18n
service (from the ngx-translation/i18n-polyfill) with Angular's new $localize
function (documentation on this is pretty limited, here 是我能找到的最好的)。我可以 运行 本地化构建并再次在特定区域设置中提供应用程序。但是,在单元测试方面我遇到了一些障碍。
以前,当使用i18n-polyfill
时,I18n
服务可以注入组件等,如下(参见I18nPolyfillComponent
):
@Component({
selector: "app-i18-polyfill",
template: `<h4>{{ title }}</h4>
})
export class I18nPolyfillComponent {
readonly title: string = this.i18n({
id: "title",
value: "Hello World!"
});
constructor(private i18n: I18n) {}
}
这可以通过在组件中注入间谍来轻松测试:
describe("I18nPolyfillComponent", () => {
let component: I18nPolyfillComponent;
let fixture: ComponentFixture<I18nPolyfillComponent>;
let mockI18n: Spy;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ I18nPolyfillComponent ],
providers: [
{
provide: I18n,
useValue: jasmine.createSpy("I18n"),
},
],
})
.compileComponents().then(() => {
mockI18n = TestBed.inject(I18n) as Spy;
});
}));
beforeEach(() => {
fixture = TestBed.createComponent(I18nPolyfillComponent);
component = fixture.componentInstance;
mockI18n.and.callFake((def: I18nDef) => def.value);
});
it("should call i18n once", () => {
expect(mockI18n).toHaveBeenCalledTimes(1);
});
});
但是,我不确定是否可以编写类似的单元测试来测试 $localize
的使用,因为它是一个全局函数而不是可注入服务。
为了完整起见,使用 $localize
(参见 I18nLocalizeComponent
),组件看起来像这样:
@Component({
selector: "app-i18n-localize",
template: `<h4>{{ title }}</h4>
})
export class I18nLocalizeComponent {
readonly title: string = $localize `:@@title:Hello World!`;
}
测试原理
我想确保我的应用程序与 I18n
/$localize
进行适当的交互(调用正确的次数,使用正确的参数等)。如果有人不小心更改了跨单位 ID 或基本翻译值,这只会防止出现愚蠢的错误。
我试过的
我试图用 test.ts
中的间谍替换全局 $localize
函数并避免导入 @angular/localize/init
:
import Spy = jasmine.Spy;
import createSpy = jasmine.createSpy;
const _global: any = typeof global !== "undefined" && global;
_global.$localize = createSpy("$localize");
declare global {
const $localize: Spy;
}
然后在测试中使用间谍 $localize
(参见:
describe("I18nLocalizeComponent", () => {
let component: I18nLocalizeComponent;
let fixture: ComponentFixture<I18nLocalizeComponent>;
let mockI18n: Spy;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ I18nLocalizeComponent ],
})
.compileComponents();
}));
beforeEach(() => {
$localize.calls.reset();
$localize.and.returnValue("Hello World!);
fixture = TestBed.createComponent(I18nLocalizeComponent);
component = fixture.componentInstance;
});
it("should call $localize once", () => {
expect($localize).toHaveBeenCalledTimes(1);
});
});
间谍确实有效,但如果组件或其他组件在其模板中使用 i18n
指令,例如 (I18nLocalizeTemplateComponent
):
<p i18n>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean consequat.</p>
这将失败并出现以下错误:
TypeError: Cannot read property 'substr' of undefined
at <Jasmine>
at removeInnerTemplateTranslation (http://localhost:9877/_karma_webpack_/node_modules/@angular/core/__ivy_ngcc__/fesm2015/core.js:34560:1)
at getTranslationForTemplate (http://localhost:9877/_karma_webpack_/node_modules/@angular/core/__ivy_ngcc__/fesm2015/core.js:34582:1)
at i18nStartFirstPass (http://localhost:9877/_karma_webpack_/node_modules/@angular/core/__ivy_ngcc__/fesm2015/core.js:34771:1)
at ɵɵi18nStart (http://localhost:9877/_karma_webpack_/node_modules/@angular/core/__ivy_ngcc__/fesm2015/core.js:34718:1)
at ɵɵi18n (http://localhost:9877/_karma_webpack_/node_modules/@angular/core/__ivy_ngcc__/fesm2015/core.js:35450:1)
at I18nLocalizeTemplateComponent_Template (ng:///I18nLocalizeTemplateComponent.js:15:9)
at executeTemplate (http://localhost:9877/_karma_webpack_/node_modules/@angular/core/__ivy_ngcc__/fesm2015/core.js:11949:1)
at renderView (http://localhost:9877/_karma_webpack_/node_modules/@angular/core/__ivy_ngcc__/fesm2015/core.js:11735:1)
at renderComponent (http://localhost:9877/_karma_webpack_/node_modules/@angular/core/__ivy_ngcc__/fesm2015/core.js:13244:1)
at renderChildComponents (http://localhost:9877/_karma_webpack_/node_modules/@angular/core/__ivy_ngcc__/fesm2015/core.js:11538:1)
Error: Expected undefined to be truthy.
at <Jasmine>
at UserContext.<anonymous> (http://localhost:9877/_karma_webpack_/src/app/i18n-localize-template/i18n-localize-template.component.spec.ts:23:23)
at ZoneDelegate.invoke (http://localhost:9877/_karma_webpack_/node_modules/zone.js/dist/zone-evergreen.js:364:1)
at ProxyZoneSpec.push../node_modules/zone.js/dist/zone-testing.js.ProxyZoneSpec.onInvoke (http://localhost:9877/_karma_webpack_/node_modules/zone.js/dist/zone-testing.js:292:1)
有没有人对如何处理这个用例有任何建议,或者目前不支持它?我还没有看到任何对此有帮助的文档、教程或文章,因此非常感谢任何帮助。
要在示例存储库中重现上述问题,您必须:
- 取消注释
test.ts
中的代码
- 在
polyfills.ts
中注释掉从 - 运行 测试
I18nLocalizeTemplateComponent
@angular/localize/init
导入
可以使用 npm run test -- --include src/app/i18n-localize-template/i18n-localize-template.component.spec.ts
或者,使用 enable-localize-unit-tests
分支,然后执行步骤 3。
备注
- 根据 Angular 升级指南,以下内容已添加到项目的
polyfills.ts
中:import "@angular/localize/init";
- 我目前正在使用 Karma (
4.4.1
) 和 Jasmine (3.5.9
) 进行单元测试
环境详细信息
_ _ ____ _ ___
/ \ _ __ __ _ _ _| | __ _ _ __ / ___| | |_ _|
/ △ \ | '_ \ / _` | | | | |/ _` | '__| | | | | | |
/ ___ \| | | | (_| | |_| | | (_| | | | |___| |___ | |
/_/ \_\_| |_|\__, |\__,_|_|\__,_|_| \____|_____|___|
|___/
Angular CLI: 9.0.6
Node: 13.2.0
OS: darwin x64
Angular: 9.0.6
... animations, cli, common, compiler, compiler-cli, core, forms
... language-service, localize, platform-browser
... platform-browser-dynamic, router
Ivy Workspace: Yes
Package Version
------------------------------------------------------------
@angular-devkit/architect 0.900.6
@angular-devkit/build-angular 0.900.6
@angular-devkit/build-ng-packagr 0.900.6
@angular-devkit/build-optimizer 0.900.6
@angular-devkit/build-webpack 0.900.6
@angular-devkit/core 9.0.6
@angular-devkit/schematics 9.0.6
@angular/cdk 9.1.3
@ngtools/webpack 9.0.6
@schematics/angular 9.0.6
@schematics/update 0.900.6
ng-packagr 9.0.3
rxjs 6.5.4
typescript 3.7.5
webpack 4.41.2
我找到的解决方案是监视 $localize
的 translate
函数而不是 $localize
本身:
test.ts
const _global: any = typeof global !== "undefined" && global;
const defaultFakedLocalizeTranslate: (messageParts: TemplateStringsArray,
substitutions: readonly any[]) => [TemplateStringsArray, readonly any[]] =
(messageParts: TemplateStringsArray, substitutions: readonly any[]) => [messageParts, substitutions];
_global.mockLocalize = createSpy("mockLocalize") as Spy;
declare global {
const mockLocalize: Spy;
}
$localize.translate = mockLocalize.and.callFake(defaultFakedLocalizeTranslate);
确保您已将 @angular/localize/init
导入 test.ts
。
I18nLocalizeComponent
单元测试可以更新如下:
describe('I18nLocalizeComponent', () => {
let component: I18nLocalizeComponent;
let fixture: ComponentFixture<I18nLocalizeComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ I18nLocalizeComponent ],
})
.compileComponents();
}));
beforeEach(() => {
mockLocalize.calls.reset();
fixture = TestBed.createComponent(I18nLocalizeComponent);
component = fixture.componentInstance;
});
it('should call $localize once', () => {
expect(mockLocalize).toHaveBeenCalledTimes(1);
});
});
此更改还将允许对模板中带有 i18n
标记的组件进行单元测试,以 运行 成功。
要检查更改,请参阅示例存储库的 fix-localize-unit-tests
分支。
找不到名字 'global'
对于在 Angular 12.运行 尝试 运行 的人。
我有以下问题
为了制作演示 运行ning 我必须添加以下内容
-> tsconfig.spec.json
将“节点”添加到编译器选项。
###tsconfig.spec.json
"compilerOptions": {
types": [
"jasmine",
"node" <====
]}
这个答案扩展了原来 post 中的测试。
任何想要测试的人
$localize 翻译了正确的字符串?
测试结果如下:
it('should call $localize.translate with "string to translate"', () => {
expect(mockLocalize.calls.argsFor(0)[0][0]).toContain('string to translate');
});
这里是Repo.
Repo 是此 post.
差异:
- 更新于 Angular 12
- 添加了翻译字符串测试