通过 @nativescript/unit-test-runner v3,我们提高了它的多功能性和易用性。我们还添加了一个简单的 CLI 标记来启用代码覆盖率报告 (--env.codeCoverage
)。在查看如何设置和运行它之前,让我们先看一个简单的测试示例。
import { isIOS } from '@nativescript/core';
class AnyClass {
hello = `hello ${isIOS ? 'from ios' : 'from android'}`;
buttonTap() {
const message = 'hello from platform native apis';
if (isIOS) {
console.log(NSString.stringWithString(message + ' on ios').toString());
} else {
console.log(new java.lang.String(message + ' on android').toString());
}
}
}
describe('Test AnyClass including platform native APIs', () => {
let anything: AnyClass;
beforeEach(() => {
anything = new AnyClass();
spyOn(console, 'log');
});
it("sanity check", () => {
expect(anything).toBeTruthy();
expect(anything.hello).toBe(`hello ${isIOS ? 'from ios' : 'from android'}`);
});
it('buttonTap should invoke platform native apis', () => {
anything.buttonTap();
expect(console.log).toHaveBeenCalledWith(
`hello from platform native apis on ${isIOS ? 'ios' : 'android'}`
);
});
});
您可以流畅地测试 iOS 或 Android 上的任何平台原生 API,并确认您的应用逻辑是否合理以及是否符合您的预期。
从 NativeScript CLI 8.1.5
版本开始(*可以使用 npm i -g nativescript
随时安装最新 CLI*),v3 单元测试运行器将在运行以下命令时自动设置
ns test init
然后,您可以为任何目标平台运行单元测试。
ns test ios
// or:
ns test android
它们默认以观察模式运行,并且运行非常高效,并持续进行实时更新。
ns test ios --env.codeCoverage
ns test android --env.codeCoverage
然后,您可以打开 coverage/index.html
文件以查看覆盖率报告。
默认情况下,报告仅引用测试涉及的文件,覆盖率百分比反映该代码集。
如果您希望在报告中包含所有代码,包括那些根本没有被测试覆盖的文件,您可以使用插件 karma-sabarivka-reporter。
安装插件
npm install --save-dev karma-sabarivka-reporter
更新 karma.conf.js
将 sabarivka 添加到报告程序数组中
reporters: [
// ...
'sabarivka'
// ...
],
向 coverageReporter 配置添加 include 属性
coverageReporter: {
// ...
include: [
// Specify include pattern(s) first
'src/**/*.(ts|js)',
// Then specify "do not touch" patterns
// (note `!` sign on the beginning of each statement)
'!src/**/*.spec.(ts|js)',
//...
]
},
下次启用覆盖率运行测试时,覆盖率百分比应反映整个代码库,并且报告应包含没有测试的文件。
如果您通过 Nx、yarn workspaces 或任何其他项目设置(在其中您在应用代码库旁边有内部管理的插件)管理自己的插件、插件套件或应用,您现在也可以轻松地收集这些测试(*甚至将它们包含在您的覆盖率报告中*)。
修改 test.ts
入口文件以包含来自**应用外部**的源代码。
import { runTestApp } from '@nativescript/unit-test-runner';
declare let require: any;
runTestApp({
runTests: () => {
// tests inside your app
const tests = require.context("./", true, /\.spec\.ts$/);
tests.keys().map(tests);
// tests outside of your app, like internally managed plugins in a workspace style setup
const pluginTests = require.context('../plugins/my-internal-plugin', true, /\.spec\.ts$/);
pluginTests.keys().map(pluginTests);
},
});
您可以探索此处演示此功能的示例仓库。
您可以在所有版本中获得改进的测试运行器的优势,但让我们以 Angular 为例,并重点介绍它在实践中的用法,使用相同的示例。
此外,我们将添加一个巧妙的 dumpView
实用程序,它将视图结构打印为字符串,我们可以用它来测试给定绑定时视图渲染是否正常工作。您可以创建任意数量对您和您的团队的测试方法有用的实用程序。例如,您可以创建一个对象来遍历视图节点进行测试,而不是创建视图绑定的字符串表示形式。
import { Component } from '@angular/core';
import { ComponentFixture } from '@angular/core/testing';
import { isIOS } from '@nativescript/core';
import { dumpView } from '../unit-test-utils';
@Component({
template: '<StackLayout><Label [text]="hello"></Label></StackLayout>',
})
class AnyComponent {
hello = `hello ${isIOS ? 'from ios' : 'from android'}`;
buttonTap() {
const message = 'hello from native apis';
if (isIOS) {
console.log(NSString.stringWithString(message + ' on ios').toString());
} else {
console.log(new java.lang.String(message + ' on android').toString());
}
}
}
describe('AnyComponent', () => {
let component: AnyComponent;
let fixture: ComponentFixture<AnyComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [AnyComponent],
}).compileComponents();
fixture = TestBed.createComponent(AnyComponent);
fixture.detectChanges();
component = fixture.componentInstance;
spyOn(console, 'log');
});
it('sanity check', () => {
expect(component).toBeTruthy();
expect(component.hello).toBe(
`hello ${isIOS ? 'from ios' : 'from android'}`
);
});
it('view binding handles iOS/Android specific behavior', () => {
expect(dumpView(fixture.nativeElement, true)).toBe(
`(proxyviewcontainer (stacklayout (label[text=hello ${
isIOS ? 'from ios' : 'from android'
}])))`
);
});
it('buttonTap should invoke native apis', () => {
component.buttonTap();
expect(console.log).toHaveBeenCalledWith([
`hello from native apis on ${isIOS ? 'ios' : 'android'}`,
]);
});
});
unit-test-utils.ts
export function dumpView(view: View, verbose: boolean = false): string {
let nodeName: string = (<any>view).nodeName;
if (!nodeName) {
// Strip off the source
nodeName = view.toString().replace(/(@[^;]*;)/g, '');
}
nodeName = nodeName.toLocaleLowerCase();
let output = ['(', nodeName];
if (verbose) {
if (view instanceof TextBase) {
output.push('[text=', view.text, ']');
}
}
let children = getChildren(view)
.map((c) => dumpView(c, verbose))
.join(', ');
if (children) {
output.push(' ', children);
}
output.push(')');
return output.join('');
}
function getChildren(view: View): Array<View> {
let children: Array<View> = [];
(<any>view).eachChildView((child: View) => {
children.push(child);
return true;
});
return children;
}
要使用 Angular 进行单元测试,您还需要确保您的主要测试入口也配置了 Angular 测试环境。
test.ts
import { runTestApp } from '@nativescript/unit-test-runner';
declare let require: any;
runTestApp({
runTests: () => {
const tests = require.context('./', true, /\.spec\.ts$/);
// ensure main.spec is included first
// to configure Angular's test environment
tests('./main.spec.ts');
tests.keys().map(tests);
},
});
main.spec.ts
import './polyfills';
import 'zone.js/dist/zone-testing.js';
import { TestBed } from '@angular/core/testing';
import { platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing';
import { NativeScriptTestingModule } from '@nativescript/angular/testing';
TestBed.initTestEnvironment(
NativeScriptTestingModule,
platformBrowserDynamicTesting(),
{ teardown: { destroyAfterEach: true } }
);
您可以使用 v3 运行器连接许多不错的集成。
例如,我们将与 SonarCloud 集成,它是一种基于云的代码质量和安全服务。
他们为公共开源项目提供免费账户,因此您可以开通一个免费账户来试用一下。
修改 coverageReporter
以包含 SonarCloud 预期的报告程序类型。
karma.conf.js
coverageReporter: {
dir: require('path').join(__dirname, './coverage'),
subdir: '.',
reporters: [
{ type: 'lcovonly' },
{ type: 'text-summary' }
]
},
npm i karma-sonarqube-unit-reporter --save-dev
现在修改 reporters
以包含 sonarqubeUnit
并为其添加配置。
karma.conf.js
// add the reporter
reporters: ['progress', 'sonarqubeUnit'],
// add the configuration
sonarQubeUnitReporter: {
sonarQubeVersion: 'LATEST',
outputDir: require('path').join(__dirname, './SonarResults'),
outputFile: 'ut_report.xml',
useBrowserName: false,
overrideTestDescription: true,
},
现在,当您执行测试时,将生成两个报告。
./coverage/lcov.info
./SonarResults/ut_report.xml
Sonarcloud 提供了一个与您的平台匹配的脚本(例如 ./sonarscan.sh
),该脚本执行分析和报告发布,他们在您设置项目时提供指导和下载链接。
对于 NativeScript,您必须将一些属性传递给 sonar 以让它知道您的配置。
sonar.typescript.tsconfigPath
Sonar 需要一个简单的 tsconfig 文件来查找所有 ts 文件。在项目的根目录创建一个单独的文件 (tsconfig.sonar.json)。
{
"extends": "./tsconfig.json",
"include": [
"./src/**/*.ts",
"**/*.d.ts"
]
}
sonar.tests
包含测试文件的目录。
sonar.test.inclusions
与测试文件匹配的文件模式。
sonar.testExecutionReportPath
测试执行报告的文件模式。
sonar.javascript.lcov.reportPaths
覆盖率报告的文件模式。
要传递的示例属性
-Dsonar.typescript.tsconfigPath=./tsconfig.sonar.json
-Dsonar.tests=./src/tests
-Dsonar.test.inclusions=**/*.spec.ts
-Dsonar.sources=./src
-Dsonar.testExecutionReportPaths=SonarResults\ut_report.xml
-Dsonar.javascript.lcov.reportPaths=coverage\**\*.info
运行扫描程序,您的配置文件现在包含单元测试和覆盖率报告。
每个人都希望能够声称这一点,但测试只能帮助减少 bug 以及提高团队对代码在团队测试条件下运行方式的信心。随着代码的发展,它们还可以帮助防止回归,因为它们为您期望以某种方式工作的区域提供了覆盖率,并且可以非常快速地告诉您,如果由于将来出现的更改而导致您期望成功的事情突然失败。
它绝对有帮助 - 在整个代码库中增加强类型检查可以帮助增强代码的完整性,这可以提高代码的持久性和适应未来变化的容易程度。但是,TypeScript 本身并不能取代适当的单元测试,因为 TypeScript 关注的是代码完整性,而单元测试则关注的是代码运行操作性和行为的逻辑成功/失败。结合使用 Eslint 和 TypeScript 可以进一步指导最佳实践的实施。